Does this app take 5 minutes to start? That's so much dynamic Spring magic. Also, how do you keep track of control flow when anything at anytime could have been overridden by something else? It seems like tracing and debugging this thing would be like exploring someone else's codebase every time.
Just rewrote it in Go. Now we are using a server which consumes 30% of ram what the existing one used to and the latency and throughput have all improved.
Don't use these stupid java backend like sprinboot.
Ops person here who has supported Java/SpringBoot applications. I think most of dislike of Java apps comes not from language or framework BUT from fact that most Java using workspaces are filled with mediocracy. They tend to be businesses with products that have extreme moats and thus quality of software barely matters. I imagine most people who would even read this medium article are dreaming of better than that.
I worked on a core Spring Boot project for five or six years at a very large enterprise. In my opinion, the most dangerous thing about this framework is that it makes its core users feel far too self-assured.
When looking at problems, your mind becomes consumed with how to force everything into design patterns—like architectural separation, DI, or interface / implementation split. This causes developers to lose sight of the actual essence of the problem because they are obsessed with conforming to the framework.
Because the ecosystem and toolchain surrounding Spring Boot and Java are so mature and well-supported, it is very easy to find community tools that make you feel like you are doing things the "right way."
I only realized these issues after I left Spring Boot and Java development behind. Now, I much prefer using TypeScript or Python to write code (for example, web servers).
I also prefer using various SaaS solutions to handle authentication and user registration rather than rebuilding it all myself with Spring Boot Security. I honestly never want to go back to the days of writing Java again.
That's fair - it does look odd in isolation. But the alternative is mixing "should this load?" conditional logic with "what should load?" bean definitions in the same class. When you're debugging why a feature didn't activate across 272 entry points, having one place to look at - just the annotations on an empty wrapper - beats scanning through 200 lines of bean definitions hunting for the one @Conditional that blocked everything. It's not magic, it's separation of concerns applied to auto-configuration. Looks weird, works well.
- Swap out Java for Kotlin. The Spring guys won't officially drop support for Java; but a lot of their recent releases are becoming very Kotlin centered. Seriously, it's much nicer to use from Kotlin. I've done both.
- Go for declarative Kotlin DSLs over annotation magic for most things. Much easier to debug. It's just function calls. And DSLs are nice in IDEs with autocomplete.
- Keep it simple. It's a huge framework. But you probably don't need most of it.
- Don't go Spring everything, a lot of stuff in Spring is a bit experimental (Spring AI/MCP stuff) or a bit bare bones (Spring Data, it's a limited and extra level of indirection you mostly shouldn't need) or flat out misguided/over-engineered (Spring Batch, good alternatives are available for that).
- Decide on synchronous or asynchronous IO. The former is a lot more scalable now that Java has green threads. The latter is relatively painless from Kotlin but an absolute PITA from Java. They are very different internally and both have their pros/cons. If you need async, Kotlin co-routines is the easier path to do that. Either way, it's one of the bigger decisions to take.
- Don't copy their way of deeply nested inheritance hierarchies. Very much in fashion 20 years ago; a bit of an anti pattern now. Internal code complexity is the part I like least about Spring. And it has some really byzantine stuff in there with 5-6 levels of inheritance or worse.
That's really the whole thesis. None of the patterns in the article are clever or novel - @ConditionalOnMissingBean has been in the Spring Boot docs since day one. What's hard is getting a team to apply it on every single bean, in every single module, for years, without anyone cutting corners. Discipline scales. Cleverness doesn't.
Good question - quite a few actually. @ConditionalOnMissingBean on every public @Bean method, proxyBeanMethods = false on every @Configuration class, the thin wrapper pattern (auto-config classes must have empty bodies) - those are all structurally checkable. CAS doesn't use ArchUnit specifically, but the consistency you see in the codebase didn't happen by accident. The patterns that can't be easily enforced statically are the harder ones - like making sure every module contributes to shared capabilities through the configurer pattern instead of direct coupling. That's where code review discipline fills the gap.
There is no right way to do Spring Boot.The entire idea is broken.
Dependency injection is good. It makes it possible to test stuff.
Automagic wiring of dependencies based on annotations is bad and horrible.
If you want to do dependency injection, you should do it the way Go programs do it. Create the types you need in your main method and pass them into the constructors that need them.
When you write tests and you want to inject something else, then create something else and pass that in.
But the idea that you create magic containers and then decorate packages or classes or methods or fields somewhere and then stuff suddenly gets wired into something else via reflection magic is a maintenance nightmare. This is particularly true when some bean is missing, and the one guy who knows which random package out of hundreds has that bean in it is on vacation and the poor schmucks on his team have no clue why their stuff doesn't work.
Manual dependency injection is fine, but it doesn't scale. Especially when you start refactoring things and dependencies need to be moved around.
The other issue is dynamic configuration. How do you handle replacing certain dependencies, e.g. for testing, or different runtime profiles? You could try to implement your own solution, but the more features you add, the closer you'd get to a custom DI framework. And then you'd have an actual mess, a naive non-standard solution for a solved problem, because you didn't want to read the manual for the standard implementation.
By the way, Spring dependency injection is mainly based on types. Annotations are not strictly necessary, you can interact with the Spring context in a procedural/functional manner, if you think that makes it better. You can also configure MVC (synchronous Servlet-based web) or Webflux (async web) routes functionally.
When a bean is missing, the app will fail to start, and you will get an error message explaning what's missing and which class depends on it. The easiest way to ensure this doesn't happen is to keep the empty @SpringBootTest test case that comes with the template. It doesn't have any assertions, but it will spin up a full Spring context, and fail if there is a configuration problem.
The only complicated part about Spring Boot is how the framework itself can be reconfigured through dependency injection. When you provide a certain "bean", this can affect the auto-configuration, so that other beans, which you might expect, are no longer automatically created. To debug this behavior, check out the relevant AutoConfiguration class (in your IDE, use the "go to class" shortcut and type something like FooAutoConfi..., e.g. JdbcAutoConfiguration).
In a good codebase, the configuration itself would be tested. For instance, if you did something a bit more complicated like connecting two JDBC databases at the same time, you would test that it read the configuration from the right sources and provides the expected beans.
Keep in mind that a big part of the configuration complexity of CAS is because of how it's used and customized in the real world.
These kinds of infrastructure systems its very common to replace a bunch of the built in functionality with your own classes, and so there's alot of effort put into supporting that use case.
I helped build a large open source enterprise financial systems, and most of the deploying orgs used CAS for authN. Both CAS and the financial system was built with similar approach to extensibility.
There's a lot of good ideas in Spring and there have been some outstanding engineers working on that framework over the past ~25 years. But it has accumulated so much baggage and relies on and perpetuates so many patterns that simply don't make sense anymore... I would love to see what the team would do with a fresh start. I wonder if they have or are considering doing a complete rewrite or starting something totally new for the next generation that will take us through the next 25 years of Java.
20 comments
[ 6.8 ms ] story [ 60.8 ms ] threadJust rewrote it in Go. Now we are using a server which consumes 30% of ram what the existing one used to and the latency and throughput have all improved.
Don't use these stupid java backend like sprinboot.
When looking at problems, your mind becomes consumed with how to force everything into design patterns—like architectural separation, DI, or interface / implementation split. This causes developers to lose sight of the actual essence of the problem because they are obsessed with conforming to the framework.
Because the ecosystem and toolchain surrounding Spring Boot and Java are so mature and well-supported, it is very easy to find community tools that make you feel like you are doing things the "right way."
I only realized these issues after I left Spring Boot and Java development behind. Now, I much prefer using TypeScript or Python to write code (for example, web servers).
I also prefer using various SaaS solutions to handle authentication and user registration rather than rebuilding it all myself with Spring Boot Security. I honestly never want to go back to the days of writing Java again.
I expected to see some new Spring 'tricks' in this post, but it's pretty much regular things that people might do in larger codebases.
- Go for declarative Kotlin DSLs over annotation magic for most things. Much easier to debug. It's just function calls. And DSLs are nice in IDEs with autocomplete.
- Keep it simple. It's a huge framework. But you probably don't need most of it.
- Don't go Spring everything, a lot of stuff in Spring is a bit experimental (Spring AI/MCP stuff) or a bit bare bones (Spring Data, it's a limited and extra level of indirection you mostly shouldn't need) or flat out misguided/over-engineered (Spring Batch, good alternatives are available for that).
- Decide on synchronous or asynchronous IO. The former is a lot more scalable now that Java has green threads. The latter is relatively painless from Kotlin but an absolute PITA from Java. They are very different internally and both have their pros/cons. If you need async, Kotlin co-routines is the easier path to do that. Either way, it's one of the bigger decisions to take.
- Don't copy their way of deeply nested inheritance hierarchies. Very much in fashion 20 years ago; a bit of an anti pattern now. Internal code complexity is the part I like least about Spring. And it has some really byzantine stuff in there with 5-6 levels of inheritance or worse.
I think this is the main point. You can write good code in any language following this concept.
Dependency injection is good. It makes it possible to test stuff.
Automagic wiring of dependencies based on annotations is bad and horrible.
If you want to do dependency injection, you should do it the way Go programs do it. Create the types you need in your main method and pass them into the constructors that need them.
When you write tests and you want to inject something else, then create something else and pass that in.
But the idea that you create magic containers and then decorate packages or classes or methods or fields somewhere and then stuff suddenly gets wired into something else via reflection magic is a maintenance nightmare. This is particularly true when some bean is missing, and the one guy who knows which random package out of hundreds has that bean in it is on vacation and the poor schmucks on his team have no clue why their stuff doesn't work.
"I added Spring Boot to our messy Java project."
"Now you have 3 problems."
The other issue is dynamic configuration. How do you handle replacing certain dependencies, e.g. for testing, or different runtime profiles? You could try to implement your own solution, but the more features you add, the closer you'd get to a custom DI framework. And then you'd have an actual mess, a naive non-standard solution for a solved problem, because you didn't want to read the manual for the standard implementation.
By the way, Spring dependency injection is mainly based on types. Annotations are not strictly necessary, you can interact with the Spring context in a procedural/functional manner, if you think that makes it better. You can also configure MVC (synchronous Servlet-based web) or Webflux (async web) routes functionally.
When a bean is missing, the app will fail to start, and you will get an error message explaning what's missing and which class depends on it. The easiest way to ensure this doesn't happen is to keep the empty @SpringBootTest test case that comes with the template. It doesn't have any assertions, but it will spin up a full Spring context, and fail if there is a configuration problem.
The only complicated part about Spring Boot is how the framework itself can be reconfigured through dependency injection. When you provide a certain "bean", this can affect the auto-configuration, so that other beans, which you might expect, are no longer automatically created. To debug this behavior, check out the relevant AutoConfiguration class (in your IDE, use the "go to class" shortcut and type something like FooAutoConfi..., e.g. JdbcAutoConfiguration).
In a good codebase, the configuration itself would be tested. For instance, if you did something a bit more complicated like connecting two JDBC databases at the same time, you would test that it read the configuration from the right sources and provides the expected beans.
These kinds of infrastructure systems its very common to replace a bunch of the built in functionality with your own classes, and so there's alot of effort put into supporting that use case.
I helped build a large open source enterprise financial systems, and most of the deploying orgs used CAS for authN. Both CAS and the financial system was built with similar approach to extensibility.