If you are referring to the dependency injection container making use of reflection, then Spring Native graduated from experimental add-on to part of the core framework some years ago. You can now opt for Quarkus/Micronaut-style static build-time dependency injection, and even AOT compilation to Go-style native executables, if you're willing to trade off the flexibility that comes with avoiding reflection. For example, not being able to use any of the "@ConditionalOnXXX" annotations to make your DI more dynamic.
(Personally, I don't believe that those trade-offs are worth it in most cases. And I believe that all the Spring magic in the universe doesn't amount to 10% of what Python brings to the table in a minimal Django/Flask/FastAPI microservice. But the option is there if your use case truly calls for it.)
Honestly, I've never run into anyone who considers Spring to be "the bane of their existence", where the real issue wasn't simply that the bulk of their experience was in something else. Where they weren't thrown into someone else's project, and resent working with decisions made by other people, but don't want to either dig in and learn the tech or else search for a new job where they get to make the choices on a greenfield project.
Debugging why something doesn't work in Spring can also be a nightmare.
And the reference point for "compile-time safe framework" is of course not Django, which is written in a dynamically typed language...
> Debugging why something doesn't work in Spring can also be a nightmare.
Making these points you can see if somebody actually used Spring in production ;)
How much time we've lost with caching in tests and other shennanigans. I've never had a stack causing more problems by just being how it is than with Spring Boot. Don't even get me started on the absolute clusterfuck of hibernate. I'd love to move on from anything from this stack and we're working on it! Absolute dumpster fire.
Every time I've seen issues where the order of test execution matters, it's either been: (1) someone one writing integration tests and calling them unit tests, or (2) transaction management issues with H2 or some other embedded test database.
For #1, you're going to have a bad time with any stack. For #2, transaction management with Spring Data JPA is admittedly a tricky subject to learn. However, most of the time you can simply slap a "@Transactional" annotation on unit test methods that mutate the database, and that will cure what ails you.
> "Debugging why something doesn't work in Spring can also be a nightmare."
I don't know what could make Spring more challenging to troubleshoot than any other Java framework, other than diagnosing issues with complex dependency injection config. And that is often grossly overblown. Most of the time, you don't HAVE to use reflection-heavy tools like "@Profile" or "@ConditionalOnXXX". Keep it simple and there's really not much magic there. And IntelliJ or any other professional-grade IDE can help to manage the magic you do choose to employ.
> "the reference point for "compile-time safe framework" is of course not Django"
I'm simply saying that in any "X Stack Considered Harmful" discussion, eventually you have to put your cards on the table and disclose which stack you are comparing X to.
Everything is written in Rust here in the imaginary fantasy world of HN. But back in the land of the real, almost all line-of-business server side API development is written in either Java/JVM, Python, .NET, Node, or rarely some PHP or Ruby holdovers. Roughly in that order. Occasional oddballs here or there using Go or something else, but not common enough to make a dent and typically very difficult to evangelise.
So with that palette available to me, I'm going paint server-side microservices with Java and Spring virtually every time. My default comparison is to Python simply because it seems the greatest rival today in terms of adoption. But if we're looking at strongly-typed compiled languages only, in the business world in practice that limits you to Java/JVM or C#/CLR. Which is like saying that Coke is the bane of your existence and your company should be drinking Pepsi.
Yeah, h2 is a very very bad idea. Hibernate and jpa are also garbage (proxies, lazy loading and transactions are just a big footgun). Have fun trying to use Sprint security and proxied hibernate objects. Somehow you have no guarantee that the class you receive and are checking and is guaranteed by code is what you expect it to be because of proxies. Absolute nightmare. We're using testcontainers, which has their issues on its own.
> I don't know what could make Spring more challenging to troubleshoot than any other Java framework > "@Profile" or "@ConditionalOnXXX"
We have one configuration with @Profile to prevent it from configuring stuff in tests. Our biggest issue is that code that compiles is not guaranteed to run or even crash when running (@Lazy anyone?). I hate this. It caused us so much pain. A bigger issue really is the automagic I was quoting. They just configure shit together because they think it's nice. Also causes a lot of headache. You have to enable/disable a random bunch of shit until it somehow works. And after they released a version fixing vulns that were open for > 1 year because of old dependencies this vodoo breaks. (Especially the garbage that is spring security. Still no migration guide for ACl in v6 huh?)
Fun fact: not a fan of Rust. It's totally overblown for web backends. I love me some Go, but having the ability to use gradle multi-modules in a ports and adpaters architecture is just plain awesome. Not sure where the journey will take us, but Kotlin (which we use exclusively) is a lot of fun. I'd love to have native Kotlin at one point exclusively. No more failing builds because of a wrong jdk or anything.
Just everything about this ecosystem is fragile af. Not sure how we got to a point where shipping a simple Go bin is easier than shipping java. Don't get me started on the resource usage and performance.
Well, I've seen other cases - mostly regarded to mock pollution. And you may say that those are poorly written tests, and maybe you're right, but the problem - again - is that there are too many footguns. Even if you don't make mistakes, your coworkers probably will.
> I don't know what could make Spring more challenging to troubleshoot than any other Java framework
Here are some examples:
- Understanding what config and which beans are applied and why (especially in tests)
- Understanding and debugging @Transactional
- Understanding and debugging Spring Security
- Debugging why a request doesn't hit the controller method you think it should (this mostly relates to the former)
> I'm simply saying that in any "X Stack Considered Harmful" discussion, eventually you have to put your cards on the table and disclose which stack you are comparing X to.
Any framework that doesn't rely on reflection and prefers explicit config over implicit magic (that doesn't mean that there can't be any default values or behaviour). For the JVM stack, that could be something like Ktor, but there's others (even in pure Java). Even in dynamically typed languages, you have options like Sinatra and Flask. They can't give you type-safety, but at least they're more easily debuggable. Even Spring itself has tried to provide an alternative with Spring Fu, but unfortunately, there seems to be little momentum and it's still experimental: https://github.com/spring-projects-experimental/spring-fu
I don't fault Spring for it, and we were on Java 11 back then with just a little bit of new hotness. Java itself just didn't lend itself to the best ergonomics.
And you could fault Ruby or any dynamic language for the same, but they usually save you a little bit of overhead or boilerplate.
This never really sounds like a Spring or Java thing. It always sounds like a "not liking dependency-injection as a general pattern" thing. My issue with that is two-fold: (1) people should just say that, and not tie it specifically to any one stack, and (2) the alternatives seem to be either monkeypatching or else writing untestable code, and those alternatives are hardly any better.
Decorators and annotations are essentialy higher-order functions and higher-order classes, but there is nothing about them that makes it intuitive. They actually become quite difficult to write and much harder to debug, OOP's shit version of a monad.
But I do want to learn Spring. What material would you recommend?
Easy: as I said down below, you can actually get wildly different classes because of things like Hibernate proxies. Also, you guessed it, the dependency injection part. I just hate it. Nothing works together. We have so many hacks and weird work-arounds because something doesn't work. (Websockets not working with tomcat for example, or the many funky troubles with using two modes of authentication at once).
> Honestly, I've never run into anyone who considers Spring to be "the bane of their existence", where the real issue wasn't simply that the bulk of their experience was in something else. Where they weren't thrown into someone else's project, and resent working with decisions made by other people, but don't want to either dig in and learn the tech or else search for a new job where they get to make the choices on a greenfield project.
In my previous startup, we used python and flask. Something I don't deem scalable for bigger teams/apps. We love Kotlin and Gradle (especially multi-modules). But there are so many drawbacks that just suck time. I have a bunch of private projects, all in Go. Fast and efficient as heck. Nothing I'd like to scale beyond maybe 5 people or 20k loc tho (no idea if bazel or something could help with that, no experience). You get a lot of good stuff, but you gain in fragility with Java/Kotlin.
Another point that just comes to mind is: how unsecure is this thing even? Dependencies are ages old, requiring you to litter your gradle build files with work arounds and overwrites so you're secure from some (often critica) vulns.
2. I don't care which programming language or framework you are using. If you hate dependency injection as a general pattern, then every alternative I've ever seen boils down to either: (1) monkeypatching all over the place to achieve the same goals, or else (2) just static hardcoding everything and not writing unit tests with any mocks. I mean, plenty of people utilize one of those approaches. They just usually don't do so while discussing safety and security with a straight face.
2. You cite Python and Go as alternatives, yet immediately acknowledge that they're unsuitable beyond small teams or apps (my God, I'll take dependency issues with Maven Central over PyPI any day of the week!). Honestly, this whole sub-thread seems to boil down to you just preferring to work on small codebases over large codebases. And that's perfectly fine! I just don't think that's language or framework-dependent.
Your only argument is that if this stack makes problems, you just haven't read enough and must be an idiot winging it. The truth is that this whole thing, especially the standard spring stack including hibernate, is just one big footgun. You can't isolate any of these, as this stack intertwines to achieve this level of footgun concentration. Some faults are hibernate, some spring's, some is due to DI, and some are due to java. And I'm fed up with the level of trickery we need to have to work with this without issues. I'd like to focus on the bugs and shitty architecture I introduce instead of others.
> (1) monkeypatching all over the place to achieve the same goals, or else (2) just static hardcoding everything and not writing unit tests with any mocks. I mean, plenty of people utilize one of those approaches. They just usually don't do so while discussing safety and security with a straight face.
These are both stupid things to do in general. If you do proper hexagonal architecture you need neither of these hacks.
Hibernate looks easy but the abstractions have a cost associated with using and maintaining it. There are a lot of settings you need to get right. There were dedicated DBAs that would optimize it in the past. You could just use JPA to make the life simpler.
Tomcat.. I mean why? It was great but I'd say it maybe went out of pace compared to everything else. Why not embedded Jetty. At this point I'm starting to have doubts about how you deploy the services to begin with.
Dependency injection is actually great albeit the usual problem is that you need to read books to understand it as there is more than one way to do everything. My pain point was usually related to the differences you need to do among Java, Groovy, Kotlin but otherwise it is awesome.
Flask is shite, basic, Python has a hard time solving its mess with dependencies and its multithreading support is meh. Go is great I love it. But if you want to create enterprise software, Java ecosystem has you covered and the engineers are cheaper to hire.
> My pain point was usually related to the differences you need to do among Java, Groovy, Kotlin but otherwise it is awesome.
Yup, agreed! We got to run kotlin pretty nice and got to play it together nicely with gradle convention plugins.
Agreed on Python. Was my first properly learned language back in the days and my first backend lang. Absolutely annoying. I also love go, but it lacks something like gradle, especially multi-module support. I love it but don't see it on a scale as our Kotlin codebase.
> engineers are cheaper to hire
I think there's also the benefit of an exotic language. You might only get 10 CVs, but you could probably hire half of them because only passionate people bother to look into exotic things. With java you have a bigger bandwidth ... which causes a lot of work.
If that's what they mean, I agree. I've seen dependency injection frameworks used in a bunch of different companies, and there are always people who consider it an essential lifesaver, like they just can't imagine working without it, and it always baffles me, because I've worked on equally large codebases that didn't use it, and it was occasionally a significant annoyance to pass dependencies by hand, but never equal to the annoyance of dealing with an automatic dependency injection framework.
This repeated experience of working with and without dependency injection, finding dependency injection to be at least as much hassle as it saves, and seeing that the people I've worked with who choose dependency injection have massively warped impressions of what it's like to work without it (they often think it's, like, impossible) has led me to see it as a tool that is driven mostly by FUD, at least at the scale of code that I have worked with.
And that's without even considering the deleterious effect that dependency injection has on design. In my cynical moments I think this is the real reason people love dependency injection. It's not that people hate the five minutes it takes to figure out how to manually pass a dependency to a module; it's that they hate the subconscious thinking that happens in those five minutes, as they see how the change affects the code, and it dawns on them that it's a code smell for every module to depend on everything else. Sometimes a dependency is a code smell, and dependency injection means you barely get a whiff, so faint you can pretend it's just your imagination. Doing it by hand means you get a few minutes to bask in the stench. You can't pretend you didn't notice.
Getting people to care about modularity, coupling, and cohesiveness in an application with dependency injection is markedly harder, just like it's harder in a language with global variables, just like it's harder to get people to think about APIs and modularity in a monolith than in microservices. And for me that's the worst part of working on codebases with pervasive dependency injection! Dependency injection is a massive liberating force for people who want to work without thinking about design. Instead of thinking about it, they just add another spaghetti dependency and keep on going, and do the same thing tomorrow, and the next day, and the next day. It's impossible to stop them! Adding a dependency is so immediate, so easy, there's no moment where they have to stop and think, "Hmmm, why am I using a dependency in module A that was only ever used in module B before? I'll need to instantiate it at the application level instead. But that means I need to pull some internal logic out of B so it can be run before B exists. Should I really be doing this? I'd better think/ask about this before I do it." Instead they just add an annotation and see if the dependency injection framework can figure it out. That's the only kind of problem I can think of that dependency injection excels at solving: problems that should never have been solved in the first place.
Your comments are confusing on whether you eschew a dependency injection framework, or eschew dependency injection as a pattern in general.
If it's the former, then you are certainly free to roll your own DI rather than leveraging a library such as Spring, Quarkus, Micronaut, Guice, Dagger, or any of the others out there. I strongly doubt that you'll implement something better than any of those. However, having written the code yourself you'll be more likely to understand it all, and that can be attractive for many people.
If it's the latter, then it's harder to take the position seriously. Look, the Java ecosystem certainly suffered from extreme OO over-engineering in the late-1990's and early-2000's, no doubt about it. A lot of people went too far overboard with enterprise-y design patterns, for sure. But that pendulum has been swinging back for over a decade now... and in the 2020's, the point of using DI is not to go crazy with the enterprise design patterns. The point is write code that can support an actual unit test suite!
You use DI so that your test suites can inject mocks or stubs, to isolate external integrations from the code under test. I don't care which language or framework you use, or whether you're using a 3rd-party DI library or implementing the approach manually yourself. If you're not using DI to accomplish this in your tests, then you're either: (1) using monkeypatching to achieve the same goal in a more dangerous manner, or (2) just writing un-testable code, and perhaps writing some integration tests that you pretend are unit tests.
My preferred approach to dependency injection is using the plain mechanisms of the language I'm using to pass dependencies as constructor and function parameters, without any framework.
I pass test fakes the same way. When doing things this way feels burdensome due to overly complex method signatures or overly complex initialization code, I look for ways to improve my design.
This works at every scale of code I've worked at. It's possible there's some size of project that it doesn't scale to, but for every project I've worked on where another engineer swore that dependency injection was absolutely necessary on the project, I've worked on a larger project where nobody ever suggested dependency injection and everything was fine.
My experience doesn't prove that dependency injection isn't necessary at some larger scale that I haven't experienced, of course, but it does convince me that the industry is rife with programmers who think they can't get by without it at scales where it is not only unnecessary but probably harmful.
Q: Why does my webapp run on most of our tomcats but not this one?
A: That tomcat server has the redis client libraries on the classpath, so Spring Boot automatically started up a connection pool connecting to localhost on the default port, and automatically includes that in the healthcheck endpoint. Since redis isn't actually running there, it fails to connect, so the healthcheck always fails and the app never shows as running, even though your code is working fine.
Q: How do I turn that off?
A: Upgrade to the next version of Spring boot and then add this undocumented annotation to your configuration
Q: How was I supposed to figure any of that out?
A: Hahaha fuck you
Spring proper is fine, useful even. Spring boot is an absolute nightmare of COME FROM style incomprehensibility.
Dagger library does that (generates all the glue code during pre-compile phase), so all the dependency-injection already happened before runtime. Also, JVM has an easier time reasoning/optimizing the code.
But I also found that writing the glue code by hand, same way as Dagger would do, is not that hard IMO.