Skip to main content

Dependency Injection

Sproogy's Dependency Injection (DI) container is the heart of the framework. Understanding how it works will help you write clean, testable code.

What is Dependency Injection?

Dependency Injection is a design pattern where objects receive their dependencies from an external source rather than creating them internally.

Without DI (Bad)

public class UserService {
private UserRepository userRepository;

public UserService() {
// Tight coupling: UserService creates its own dependency
this.userRepository = new UserRepositoryImpl();
}
}

Problems:

  • Hard to test (can't replace with a mock)
  • Hard to configure (connection string is hardcoded somewhere)
  • Hard to change (switching implementations requires code changes)

With DI (Good)

@Service
public class UserService {
private final UserRepository userRepository;

@Autowired
public UserService(UserRepository userRepository) {
// Loose coupling: dependency is injected
this.userRepository = userRepository;
}
}

Benefits:

  • Easy to test (pass a mock repository)
  • Easy to configure (Sproogy injects the configured instance)
  • Easy to change (swap implementations with @Qualifier or configuration)

How Sproogy's DI Works

Sproogy uses a graph-based dependency resolution algorithm to instantiate beans in the correct order.

Step 1: Component Scanning

When you call SproogyApp.run(), Sproogy scans your package for annotated classes:

@SproogyApplication(basePackage = "com.example")
public class Main {
public static void main(String[] args) {
SproogyApp.run(Main.class, args);
}
}

Sproogy scans:

  • com.example (your package)
  • com.sproogy (framework packages)

It looks for these stereotypes:

  • @Component
  • @Service
  • @Controller
  • @Repository
  • @Configuration

Step 2: Dependency Graph Construction

Sproogy builds a directed graph of dependencies:

Step 3: Cycle Detection

Sproogy detects circular dependencies at startup (not at runtime):

// This will FAIL at startup
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB; // ServiceA depends on ServiceB
}

@Service
public class ServiceB {
@Autowired
private ServiceA serviceA; // ServiceB depends on ServiceA ❌ CYCLE!
}

Error message:

Circular dependency detected: ServiceA → ServiceB → ServiceA

Step 4: Topological Sort

Dependencies are instantiated in the correct order (leaf nodes first):

Instantiation Order:
1. MailConfig (no dependencies)
2. DataSource (no dependencies)
3. EmailService (depends on MailConfig)
4. UserRepository (depends on DataSource)
5. UserService (depends on UserRepository, EmailService)
6. UserController (depends on UserService)

Step 5: Field Injection

After instantiation, Sproogy injects @Autowired, @Value, and @Env fields using reflection.

Injection Types

@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;

@Autowired
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
}

Pros:

  • Dependencies are immutable (final fields)
  • Easy to test (just pass mocks to constructor)
  • Dependencies are explicit and required

Cons:

  • More verbose for many dependencies

Field Injection

@Service
public class UserService {

@Autowired
private UserRepository userRepository;

@Autowired
private EmailService emailService;
}

Pros:

  • Less verbose
  • No constructor needed

Cons:

  • Dependencies are mutable (not final)
  • Harder to test (need reflection to set fields)
  • Circular dependencies harder to spot
Best Practice

Prefer constructor injection for required dependencies. Use field injection sparingly for optional dependencies.

Bean Scopes

Singleton (Default)

One instance per application:

@Service // Singleton by default
public class UserService {
// Only ONE instance created and shared
}

Lazy Initialization

Defer bean creation until first use:

@Component(lazy = true)
public class HeavyResource {
public HeavyResource() {
System.out.println("Expensive initialization...");
}
}

Without lazy = true, this is created at startup. With lazy = true, it's created only when first injected.

Handling Multiple Implementations

Problem: Multiple Beans of Same Type

@Component
public class MySQLUserRepository implements UserRepository { }

@Component
public class PostgreSQLUserRepository implements UserRepository { }

@Service
public class UserService {
@Autowired
private UserRepository userRepository; // ❌ Which one to inject?
}

Solution 1: @Primary

Mark one bean as the default:

@Component
@Primary // This will be injected when no qualifier is specified
public class MySQLUserRepository implements UserRepository { }

@Component
public class PostgreSQLUserRepository implements UserRepository { }

@Service
public class UserService {
@Autowired
private UserRepository userRepository; // ✅ Injects MySQLUserRepository
}

Solution 2: @Qualifier

Specify which bean to inject by name:

@Service
public class UserService {

@Autowired
@Qualifier("mySQLUserRepository") // Inject by bean name
private UserRepository userRepository;
}

Bean naming:

  • MySQLUserRepository → bean name is "mySQLUserRepository" (lowercase first letter)
  • Or specify explicitly: @Component("customName")

Solution 3: @NonPrimary

Explicitly mark a bean as non-primary:

@Component
public class MySQLUserRepository implements UserRepository { }

@Component
@NonPrimary
public class PostgreSQLUserRepository implements UserRepository { }

Configuration Classes

Define beans programmatically:

@Configuration
public class AppConfig {

@Bean
public DataSource dataSource(@Env("DB_URL") String url) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setUsername(System.getenv("DB_USER"));
config.setPassword(System.getenv("DB_PASSWORD"));
return new HikariDataSource(config);
}

@Bean
@Primary
public UserRepository userRepository(DataSource dataSource) {
return new MySQLUserRepository(dataSource);
}
}

How it works:

  1. Sproogy scans for @Configuration classes
  2. Detects methods annotated with @Bean
  3. Calls these methods and registers return values as beans
  4. Method parameters are injected automatically

Injecting Configuration Values

From Environment Variables (@Env)

@Service
public class DatabaseService {

@Env("DB_URL")
private String databaseUrl;

@Env("DB_USER")
private String username;

@Env("DB_PASSWORD")
private String password;
}

Source: .env file or system environment variables

# .env
DB_URL=jdbc:mysql://localhost:3306/mydb
DB_USER=admin
DB_PASSWORD=secret

From Application File (@Value)

@Service
public class SocketService {

@Value("application.socket.format.idKey")
private String idKey; // Reads from application.yml

@Value("application.socket.format.payloadKey")
private String payloadKey;
}

Source: application.yml or application.json

# application.yml
application:
socket:
format:
idKey: "id"
payloadKey: "data"

Injecting the Entire Configuration

@Service
public class ConfigService {

@Autowired
@Qualifier("applicationFile")
private JsonNode config; // Entire application.yml as JsonNode

public String getValue(String path) {
return config.at("/" + path.replace(".", "/")).asText();
}
}

Dependency Injection Lifecycle

Advanced: Programmatic Bean Retrieval

Access beans from the ApplicationContext:

@Service
public class DynamicService {

@Autowired
private ApplicationContext context;

public void doSomething() {
// Get bean by type
UserService userService = context.getBean(UserService.class);

// Get bean by name
Object bean = context.getBean("userService");

// Get bean by name and type
UserService service = context.getBean("userService", UserService.class);
}
}
warning

Programmatic bean retrieval is an anti-pattern in most cases. Prefer @Autowired for static dependencies.

Use ApplicationContext.getBean() only for:

  • Dynamic bean selection based on runtime conditions
  • Lazy loading of heavy resources
  • Plugin architectures

Testing with Dependency Injection

Dependency injection makes unit testing trivial:

// Production code
@Service
public class UserService {
private final UserRepository userRepository;

@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}

public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
}

// Test code (JUnit + Mockito)
public class UserServiceTest {

@Test
public void testFindById() {
// Create mock repository
UserRepository mockRepo = Mockito.mock(UserRepository.class);
User expectedUser = new User(1L, "John");
Mockito.when(mockRepo.findById(1L)).thenReturn(Optional.of(expectedUser));

// Inject mock via constructor (no Sproogy needed!)
UserService service = new UserService(mockRepo);

// Test
User actualUser = service.findById(1L);
assertEquals(expectedUser, actualUser);
}
}

Common Pitfalls

1. Forgetting @Autowired on Constructor

@Service
public class UserService {
private final UserRepository userRepository;

// ❌ Missing @Autowired - repository will be null!
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}

Fix: Add @Autowired:

@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}

2. Circular Dependencies

@Service
public class A {
@Autowired
private B b;
}

@Service
public class B {
@Autowired
private A a; // ❌ Cycle!
}

Fix: Refactor to remove the cycle (extract shared logic to a third class).

3. Not Scanning the Right Package

// Main class in com.example.app
@SproogyApplication(basePackage = "com.example.app")
public class Main { }

// Service in com.example.service (DIFFERENT package!)
@Service // ❌ Won't be found!
public class UserService { }

Fix: Use a common base package:

@SproogyApplication(basePackage = "com.example")
public class Main { }

4. Multiple Beans Without @Primary or @Qualifier

@Component
public class Impl1 implements MyInterface { }

@Component
public class Impl2 implements MyInterface { }

@Service
public class MyService {
@Autowired
private MyInterface myInterface; // ❌ Which one?
}

Fix: Add @Primary to one or use @Qualifier.

Next Steps

Now that you understand dependency injection, learn about the application lifecycle:

👉 Application Lifecycle

Or explore all available annotations:

👉 Annotations Reference