Tag: autowired

  • Mastering IoC in Java

    Mastering IoC in Java

    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