How We Moved Our Team to Manual DI Injection Instead of a Framework (Retrospective + Lessons Learned)
Last year, our team worked on cleaning up Spring dependency injection in our legacy codebase by moving to manual constructor injection. The results alone are satisfying, but we made many mistakes along the way, so I'm writing this down for the record.
The background was like this: field injection was everywhere, circular references were tangled in 4–5 places, and writing tests required spinning up the entire context.
The process went roughly like this:
1. First, we mapped the circular reference graph to identify the problem areas first (we lost two weeks here)
2. Enforced constructor injection for new code first, and cleaned up existing code when we touched it
3. Set a rule prohibiting refactoring of modules without tests
4. Finally, cleaned up all remaining field injections in one pass
The main thing I regret is spending too much time on step 1. If we had narrowed the scope from the start to "only fix what we can fix," I think we would have saved two months. With refactoring, defining the scope always seems to be half the battle.
8 answers
I really agree with the saying that defining the scope is half the battle.
How did you handle the bulk cleanup of field injection? We removed everything with @Autowired all at once, and the app wouldn’t even boot—we wasted half a day ㅠㅠ
The reflection part is the most valuable part of this post. Everyone only writes success stories, but it’s rare for someone to also write about wasting two weeks. I’m saving this.
I like the approach of enforcing it starting with new code. We did the opposite—tried to overhaul everything at once, and the branch stayed alive for three weeks with 400-line reviews, which just soured the team's mood lol
What pattern did you use to resolve the circular references? I’m curious whether you broke them with events or separated the interfaces. Looking forward to the follow-up post.
Two weeks just to draw a circular reference graph is a bit... honestly, wouldn't that take like 30 minutes if you run an IDE plugin? Though I guess legacy code would be an exception lol
We've done similar work, and rule #3 is the most important. We touched a module with no tests, couldn't roll back, and ended up shipping three hotfixes. Since then, we've made "don't touch files with 0 test coverage" a hard team rule. Without that, it's not refactoring—it's gambling.
Well, honestly, I’m a bit skeptical about this. Constructor injection isn’t always the answer, and if you have four or five circular references, that’s a sign to question the structure itself—it’s not something that changing the injection method is going to solve. Isn’t the root cause somewhere else?