Mastering IoC in Java

10 IOC technics

10 Powerful Features Explained with STAR Method, SOLID Principles & Real Code

Understanding Inversion of Control (IoC) is essential for writing maintainable, scalable, and testable Java applications. This post explores 10 IoC features using real-world situations, and shows how they align with SOLID principles — with real code to bring everything to life.


1. Decoupled Code

  • Situation: You have a BillingService that directly creates and uses a PaymentGateway, making testing and replacement hard.
  • Task: Allow BillingService to work independently of specific gateway implementations.
  • Action: Extract PaymentGateway to an interface and inject it from outside.
  • Result: BillingService now works with any payment gateway, even mocks for testing.

SOLID Alignment

  • S (SRP): Each class has a single responsibility.
  • D (DIP): High-level modules do not depend on low-level modules.

Code Example

// STEP 1: Define an abstraction (interface) for the payment behavior
public interface PaymentGateway {
    // This method allows any implementation to process a payment
    void charge(String accountId, double amount);
}

// STEP 2: Provide a concrete implementation of the interface
public class StripeGateway implements PaymentGateway {

    @Override
    public void charge(String accountId, double amount) {
        // Simulating a call to Stripe's payment processing system
        System.out.println("Charging $" + amount + " to account " + accountId + " via Stripe.");
    }
}

// STEP 3: The BillingService depends on the abstraction, not a specific implementation
public class BillingService {

    // This is the dependency (PaymentGateway), but we're not creating it here
    private final PaymentGateway gateway;

    // Constructor Injection: we receive the dependency from outside
    public BillingService(PaymentGateway gateway) {
        this.gateway = gateway;
    }

    // This method can now work with ANY PaymentGateway implementation
    public void processPayment(String accountId, double amount) {
        System.out.println("BillingService: Processing payment...");
        gateway.charge(accountId, amount);
    }
}

// STEP 4: Simulate an application context that wires everything together
public class Main {
    public static void main(String[] args) {
        // Instead of hardcoding dependencies inside BillingService,
        // we create them here — acting like a basic IoC container

        PaymentGateway gateway = new StripeGateway(); // could be replaced with another gateway
        BillingService billingService = new BillingService(gateway);

        // Now we use the service as usual
        billingService.processPayment("acct-123", 49.99);
    }
}

2. Dependency Injection

  • Situation: EmailService internally creates a SmtpClient, causing tight coupling.
  • Task: Enable the injection of a flexible mail client.
  • Action: Use constructor injection.
  • Result: The service can be used with mocks or real implementations.

SOLID Alignment

  • O (OCP): You can add new email clients without modifying EmailService.

Code Example

public interface MailClient {
    void sendEmail(String to, String subject, String body);
}

public class SmtpClient implements MailClient {
    public void sendEmail(String to, String subject, String body) {
        System.out.println("Sending email via SMTP...");
    }
}

public class EmailService {
    private final MailClient mailClient;

    public EmailService(MailClient mailClient) {
        this.mailClient = mailClient;
    }

    public void notify(String userEmail) {
        mailClient.sendEmail(userEmail, "Welcome", "Thanks for registering!");
    }
}

3. Centralized Configuration

  • Situation: All classes instantiate their dependencies manually.
  • Task: Centralize object creation and wiring.
  • Action: Use a configuration class or framework like Spring.
  • Result: A single source of truth for dependencies.

SOLID Alignment

  • S (SRP): Objects focus on behavior, not on instantiation logic.

Code Example (Spring-style config)

@Configuration
public class AppConfig {

    @Bean
    public MailClient mailClient() {
        return new SmtpClient();
    }

    @Bean
    public EmailService emailService() {
        return new EmailService(mailClient());
    }
}

4. Lifecycle Management

  • Situation: Developers manually initialize and destroy resources.
  • Task: Automate resource lifecycle hooks.
  • Action: Use annotations like @PostConstruct and @PreDestroy.
  • Result: Cleaner and safer resource handling.

SOLID Alignment

  • S (SRP): Initialization logic is separated and container-managed.

Code Example

@Component
public class FileCache {

    @PostConstruct
    public void init() {
        System.out.println("Initializing cache...");
    }

    @PreDestroy
    public void shutdown() {
        System.out.println("Cleaning up cache...");
    }
}

5. Event Propagation

  • Situation: Components directly call each other to handle events.
  • Task: Decouple them using event broadcasting.
  • Action: Use an event publisher/subscriber model.
  • Result: Independent, modular event handling.

SOLID Alignment

  • O (OCP), D (DIP): Listeners can be added without modifying publishers.

Code Example (Spring-style)

public class UserRegisteredEvent extends ApplicationEvent {
    public final String email;
    public UserRegisteredEvent(Object source, String email) {
        super(source);
        this.email = email;
    }
}

@Component
public class WelcomeEmailListener {
    @EventListener
    public void onUserRegistered(UserRegisteredEvent event) {
        System.out.println("Sending welcome email to " + event.email);
    }
}

6. AOP (Aspect-Oriented Programming) Integration

  • Situation: Logging, security, or transactions clutter core logic.
  • Task: Separate cross-cutting concerns.
  • Action: Use AOP to define reusable, injectable behavior.
  • Result: Core logic stays clean and focused.

SOLID Alignment

  • S (SRP): Separate concerns into aspects.

Code Example

@Aspect // Marks this class as an Aspect
@Component // Enables Spring to manage this bean
public class LoggingAspect {

    // Pointcut expression for any method in the service package
    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        // Logging before method execution
        System.out.println("LOG: Calling method " + joinPoint.getSignature().getName() +
            " with arguments: " + java.util.Arrays.toString(joinPoint.getArgs()));
    }
}

7. Bean Scoping

  • Situation: You need a new object each time in one context, but not in another.
  • Task: Define the scope of object lifecycles.
  • Action: Use @Scope to specify scope.
  • Result: Proper reuse or creation based on context.

SOLID Alignment

  • S (SRP): Classes don’t manage their own lifecycles.

Code Example

@Component
@Scope("prototype")
public class CommandHandler {
    public void execute() {
        System.out.println("Executing command...");
    }
}

8. Lazy Initialization

  • Situation: Heavy objects are created eagerly, even when unused.
  • Task: Load only when needed.
  • Action: Use @Lazy.
  • Result: Faster startup and lower memory usage.

SOLID Alignment

  • I (ISP): Only needed dependencies are loaded when required.

Code Example

@Component
@Lazy
public class AnalyticsService {
    public AnalyticsService() {
        System.out.println("Expensive analytics setup...");
    }
}

9. Auto-wiring

  • Situation: Manually injecting dependencies becomes tedious and error-prone.
  • Task: Let the framework resolve dependencies automatically.
  • Action: Use @Autowired.
  • Result: Cleaner code and fewer wiring bugs.

SOLID Alignment

  • D (DIP): Classes depend on interfaces and are wired via configuration.

Code Example

@Component
public class ReportService {

    @Autowired
    private DataSource dataSource;

    public void generate() {
        System.out.println("Generating report using " + dataSource);
    }
}

10. Profile and Environment Support

  • Situation: Your app needs different behavior for dev, test, and production.
  • Task: Isolate configuration per environment.
  • Action: Use Spring’s @Profile.
  • Result: Clean environment separation.

SOLID Alignment

  • O (OCP): Add new environments without changing the core logic.

Code Example

@Profile("dev")
@Component
public class DevMailClient implements MailClient {
    public void sendEmail(String to, String subject, String body) {
        System.out.println("DEV MODE - Email to " + to);
    }
}

Conclusion

IoC isn’t just a theoretical concept — it’s a practical toolkit that solves real-world Java development challenges. These 10 features, when combined with SOLID principles, give you the power to build software that’s maintainable, flexible, and testable.

Each feature has its place — and now you have a guide to applying them with real-world clarity.

More information about and example can be found here