Stop Hardcoding Dependencies — Embrace Dependency Injection
Why Your Hard-Coded Dependencies Are Killing Your Code Quality
Celebrate 15h of August with 35% off on our paid subscription — limited-time offer, don’t miss out!
Hello guys, in the fast-paced world of software development, small coding decisions can have huge long-term consequences.
One of the most common mistakes developers make—especially in the early stages of a project—is hardcoding dependencies directly into classes.
It works at first, but as the project grows, it becomes a tangled mess that’s difficult to maintain, test, and extend.
Dependency Injection (DI) offers a cleaner, more scalable solution. By decoupling classes from their dependencies, DI makes your code flexible, testable, and far easier to maintain.
In the last article I talked about SOLID principle violations and this article, we’ll explore why you should avoid hardcoding dependencies, look at before-and-after examples, and analyze the pros and cons of embracing dependency injection.
By the way, I am also running a sale and you can get our paid plan for good 35% discount. Instead of paying 50$ / year, you pay 32.5$ / year (only 3$ / month)! That’s the price of less than one cup of decent coffee.
Here are the benefits you unlock with a paid subscription:
Get access to paid subscribers posts.
Full archive of 220+ posts on System Design, Coding, Java, AI, and LLM
Many expense it with team's learning budget
The Problem: Hardcoding Dependencies
If you have been coding for few years then you have most likely come across following code multiple times but if not, just imagine you are building a UserService
class that sends a welcome email to new users.
❌ Before: Hardcoded Dependencies
public class UserService {
private EmailService emailService;
public UserService() {
this.emailService = new EmailService(); // Hardcoded dependency
}
public void registerUser(String email) {
// Logic to save user
emailService.sendEmail(email, "Welcome to our platform!");
}
}
class EmailService {
public void sendEmail(String to, String message) {
System.out.println("Sending email to " + to + ": " + message);
}
}
Now, just wear your code reviewer hat and try to review the code and see if you can spot few improvements before reading further?
What’s wrong here?
Tight Coupling –
UserService
is directly tied toEmailService
.Hard to Test – You can’t easily replace
EmailService
with a mock in unit tests.Poor Scalability – If you later want to send SMS instead of email, you must modify
UserService
.Single Responsibility Violation –
UserService
is now responsible for knowing how to create anEmailService
. That’s the violation of SOLID we talked in last post.
The Solution: Dependency Injection
With dependency injection, we pass the dependency into the class instead of creating it inside.
✅ After: Using Constructor Injection
public class UserService {
private EmailService emailService;
public UserService(EmailService emailService) {
this.emailService = emailService; // Injected dependency
}
public void registerUser(String email) {
// Logic to save user
emailService.sendEmail(email, "Welcome to our platform!");
}
}
class EmailService {
public void sendEmail(String to, String message) {
System.out.println("Sending email to " + to + ": " + message);
}
}
// Usage
public class App {
public static void main(String[] args) {
EmailService emailService = new EmailService();
UserService userService = new UserService(emailService);
userService.registerUser("test@example.com");
}
}
Benefits of this approach:
Loose Coupling –
UserService
no longer creates its ownEmailService
.Easier Testing – You can pass a
MockEmailService
for unit tests.Flexible – You can inject different implementations without changing
UserService
.Follows SOLID Principles – Specifically, the Dependency Inversion Principle.
By the way, nowadays you can even integrate AI code review tools like CodeRabbit to catch Dependency Injection and SOLID violations automatically.
Alternative: Interface-Based Injection
While the second version fixes a lot of issues like its easy to test and loosely coupled as well as follows the SOLID principle, we can this system even more flexible by injecting an interface instead of a concrete class.
public interface MessageService {
void sendMessage(String to, String message);
}
public class EmailService implements MessageService {
@Override
public void sendMessage(String to, String message) {
System.out.println("Sending email to " + to + ": " + message);
}
}
public class SMSService implements MessageService {
@Override
public void sendMessage(String to, String message) {
System.out.println("Sending SMS to " + to + ": " + message);
}
}
public class UserService {
private MessageService messageService;
public UserService(MessageService messageService) {
this.messageService = messageService;
}
public void registerUser(String contact) {
// Logic to save user
messageService.sendMessage(contact, "Welcome to our platform!");
}
}
With this setup:
Switching from Email to SMS requires no change in
UserService
.The class is open for extension but closed for modification (OCP from SOLID).
Pros and Cons of Dependency Injection
✅ Pros
Loose Coupling – Your classes aren’t tied to specific implementations.
Testability – Easier to replace real services with mocks/stubs in tests.
Flexibility – You can switch implementations at runtime.
Readability – Dependencies are explicit in the constructor.
⚠️ Cons
Initial Complexity – Beginners may find DI patterns harder to grasp.
More Setup – Requires additional wiring, especially without a DI framework.
Overengineering Risk – For small scripts, DI might be unnecessary.
When to Use Dependency Injection
I have been using DI even since I come across Spring Framework and with increased focused on Unit testing and 100% code coverage, I am using DI almost all the code I am writing, only exceptions are Python and bash scripts.
But here are guidelines from experts won when to use Dependency Injection
Large, long-lived applications where requirements evolve.
Test-driven development environments.
Systems with multiple interchangeable components.
Microservices or modular architectures.
For small, throwaway scripts, the overhead might not be worth it. But for anything that grows beyond a few classes, dependency injection will save you countless headaches.
Conclusion
Hardcoding dependencies feels quick and easy—but it’s a trap. Over time, it turns your codebase into a rigid, untestable structure that’s expensive to maintain. Dependency Injection, on the other hand, promotes flexibility, maintainability, and testability.
By adopting DI early, you’re investing in a clean architecture that can evolve with your project, rather than fighting against it later.
What's more, AI-powered tools like CodeRabbit are increasingly becoming valuable allies in this process.
CodeRabbit can spot hardcoded dependencies during a pull request review by analyzing the code’s structure and identifying instances where classes directly instantiate other concrete classes instead of relying on abstractions or interfaces.
For example, if a constructor uses new SomeService()
instead of accepting a SomeService
interface through dependency injection, CodeRabbit’s static analysis and AI-assisted review can flag this as a violation of the Dependency Inversion Principle.
It can then provide inline comments suggesting refactoring to inject the dependency via constructor or method parameters. This automated detection helps maintain cleaner, testable, and more maintainable code, catching issues before they make it to production.
That’s all friends, if you like this article then please share and repost so that more people can see it and benefit from it. I really appreciate your help there.
Here is the summary of this in a single diagram:
Celebrate 15h of August with 35% off on our paid subscription — limited-time offer, don’t miss out!
Other AI, System Design, and Clean Code Articles you may like
Using the interface approach is cleaner, as it removes the need to worry about extending new message services in the future and makes it easier to create mock classes