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 aPaymentGateway
, 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 aSmtpClient
, 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