Anyways, just my two cents as a almost decade long "enterprise" java developer, who has used his fair share of netflix packages as well.
Anyways, just my two cents as a almost decade long "enterprise" java developer, who has used his fair share of netflix packages as well.
Wait, what?
I spend my life nowadays writing boilerplate in Go. The language and ecosystem is riddled with boilerplate and needing to repeat yourself. That said, I find the language plenty productive, but concise and reusable Go is not.
The rest of your post has nothing to do with the Java programming language and instead has to do with your frustration at the Java enterprise ecosystem, the desire to make things abstract for extendibility etc etc... considering Go is now being adopted heavily in those same enterprise shops and often being written by former Java developers I predict it will suffer a similar souring of public opinion by 2030 or so.
Enterprise Go is coming.
I think Go will not ever be as bad as Enterprise Java because language and community culture which is shaped by language, don't give you as much opportunity to abuse it. If Go gets some new features that may enable abusing it, then yeah, history will repeat :)
Or you could just not do that. The Java culture is heavy on those things, but nothing forces you to participate in this if you're building from the ground-up.
Nothing in Java drives you to write bloated code with tons of classes. javac will not throw an error if you write proper simple code.
If you chose, or were forced by bad team culture somewhere, to write 20 classes for what should've been one function, that's a people problem. Java didn't make you do it.
That team who loves complex overengineering so much will do the same thing in whatever language they switch to someday.
In many cases, that is a function. Functions you can keep mostly on the same level of nested-ness. Occasionally you will have a higher-order thingy. A class, which gets instanciated but then the instance is not used? Well, that's a code smell. Probably not a real class you're dealing with, but someone had the urge to hammer it into a class thingy. Then the next person will come along and inherit from that and the one after that will write an adapter around it and so it goes on, until a degree of complexity is reached, that is mind-boggling.
A lot of classes also disappear when you simply create a struct and write the functions that deal with the struct's members, decoupling state and behavior. Of course this is not how it is done in mainstream OOP. Some language have adopted this kind of approach. For example Rust with structs and traits being implemented for a struct separately.
Hypothetically speaking, when using the concept of a class, one would expect an instance creation somewhere and that instance to be actually used. If it is not used after creation, then that means, that the constructor can probably be written as a function, saving one level of nesting and also using a simpler concept to express the same thing. When looking at a class, I might need to also consider what its base/super class is and might need to look at what members and methods that one has. I might have to look at what interfaces it implements. When I see a class, I expect some kind of state to be stored in it. If there is no state or only a single attribute, then that might be another sign, that I am not actually dealing with a thing, that needs to be a class.
A class is quite a complex thing, wherein one needs to look out for a lot of things.
Often making everything a class comes with another disadvantage when writing unit tests. You always have to instantiate (assuming not everything is static) and, if you hold state in member variables, you need to account for that in tests, covering various values of that member variable. Writing things as a class makes it easy to have side-effects updating your object internal state. This can make testing even more difficult.
There is complexity, which is inherent to a problem, and there is complexity created by people trying to solve problems with more complex than necessary means. Use a class when necessary and useful. Not for everything that does not hide on the count of 3.
For the use I take from your question, I would recommend simply using namespaces for implementing namespaces. If the language does not provide such, then see if there are modules, which serve as namespaces. Only as a last resort use a class to build a namespace. In that case I would already ask myself, why my language does not support something as basic as a namespace.
But viewed as a namespace, there is at least no global namespace pollution.
Modern Java is a much more pleasant developer experience than that. For extremely large applications it also comes with the benefit of being able to remove the boilerplate and ceremony required to do basic things like call an internal service, internal service auth, rate limiting, circuit breaking etc.
Most of the criticism about Java seem to be about old codebases that overused Gang of Four patterns, when that used to be more of a thing. Don’t blame the hammer for the shoddy building, blame the hammerer.
I'm happy I don't have to work with any kind of Java any more.
Are you 10 years behind? Modern Java moves the complexity into compile time annotation processors, so it's even more difficult to figure out what's happening when something breaks.
For better or worse, Go is where all the energy is in terms of OSS, so I do hope I see the light some day, but for now, the dev x for me is pretty underwhelming compared to Java
otoh, you are totally right about the 8 layers deep root functionality, but that IMHO reduces to mindless application of textbook oop patterns to applications (I still have to see a company randomly "changing databases" so the 8 layers of abstraction over the db are correct).
still, I adhere to a lost standard these days, which is less code as possible. golang is automatically disqualified since it thinks that a 8 LoC for loop with temp variables is "simpler" than a .map(fn) (so much for "throwing boilerplate away"). it likes to introduce point of failures, and I dislike point of failures
If you chose to write that kind of code and then hate it, that's all on you. Neither the Java language spec nor the JDK make any effort to steer you into such baroque complexity. Keep it simple, best Java code is very simple.
I wrote Java for about 20 years (many of them at Sun) and have never written a AbstractFactorySingletonProxyFactoryBeans or anything like it. Don't do it.
And before the inevitable, a map[T]interface{} only provides part of what a real set does. The rest is for loops.