In 2012, when I first joined Global CyberSoft and entered the software development industry full of bewilderment :sweat_smile:, two concepts—Inversion of Control (IoC) and Dependency Injection (DI)—were the toughest for me to grasp. Yet they turned out to be the foundation for modern application architecture. Let’s explore why they are so essential!

Table of Contents:
- Dependency problem in OOP?
- What is Inversion of Control?
- What is Dependency Injection?
- Implement Salary Transfer with Dependency Injection
1. Dependency problem in OOP
In Object-Oriented Programming (OOP), a major challenge in large applications is tight coupling between classes. Tight coupling makes software difficult to change and extend, and makes Unit Testing nearly impossible for classes with hardcoded dependencies.
Sounds abstract? Let’s revisit the example from Custom Exception to make it crystal clear.
Below is the class diagram:
Suppose that in addition to
Money Transfer, other features like Money Topup, Money Withdraw, Money Exchange Rates… also depend directly on VietcomBank and VietinBank. If there is a requirement to swap VietcomBank with another bank, we must modify the source code of every dependent class (Money Transfer, Money Topup, Money Withdraw, Money Exchange Rates…). Imagine a system with dozens or hundreds of classes and hundreds of dependencies: even a “minor requirement change” ripples into massive code churn with high regression risk.
To address this problem, software engineering introduced the design principle known as Inversion of Control.
2. What is Inversion of Control?
In software engineering, inversion of control (IoC) is a programming principle. IoC inverts the flow of control as compared to traditional control flow. In IoC, custom-written portions of a computer program receive the flow of control from a generic framework. A software architecture with this design inverts control as compared to traditional procedural programming: in traditional programming, the custom code that expresses the purpose of the program calls into reusable libraries to take care of generic tasks, but with inversion of control, it is the framework that calls into the custom, or task-specific, code.
— Wikipedia
A bit overwhelming, right? ;))
In simple terms, IoC is the idea of “inverting” traditional control flow: instead of individual classes declaring and instantiating their own dependencies, this responsibility is delegated to a centralized framework. In other words, a single authority manages dependency lifecycles and wiring.
Advantages of IoC:
- Decouples task execution from coordinator/caller logic. As in the Money Transfer example, the
Money Transferclass doesn’t need to know the internal details ofVietcomBankandVietinBank. - Easily switch implementations (e.g., swapping
VietcomBankfor a new bankTechcomBankvia configuration). - Fosters modular application architecture.
- Enables
Testable Classesby allowing test doubles (mocks/stubs) to be injected during tests (covered in detail in theUnit Testseries).
Trade-offs / Disadvantages:
- Dependency on a framework.
- Steeper learning curve for newcomers.
- Some configuration errors shift from compile-time to runtime.
- Slightly slower startup time due to classpath scanning and dependency graph instantiation.
3. What is Dependency Injection?
IoC is a design principle. There are multiple ways to implement it, with Dependency Injection (DI) being the most prominent. How does DI implement IoC?
To achieve IoC, DI typically consists of these key components:
- Client: The class that depends on other services.
- Service: The interface or abstract class required by Clients.
- Service Implement: The concrete implementation class of the Service.
- Injector: The component responsible for instantiating Clients and Service Implementations, and injecting the appropriate dependencies into Clients based on declared wiring rules.
Here is the step-by-step workflow:
4. Implement Salary Transfer by Dependency Injection
I converted all example code from Money Transfer from Java to Kotlin.
For the DI framework, I chose Spring because it remains the most powerful and widely used framework in the JVM ecosystem. The build.gradle.kts configuration is available here.
Since the classes SalaryTransfer, MoneyTransfer, and SalaryCalculator are components of the application, we annotate them with @Component.
For VietcomBank and VietinBank, both implement the Bank interface. If we simply annotated both with @Component, the Spring Injector wouldn’t know which one to inject into the MoneyTransfer client. Therefore, we declare them explicitly in IocdiApplication:
| |
Looking at the constructor of MoneyTransfer:
| |
The Spring Injector matches bean names to parameter names, injecting VietcomBank into primaryBank and VietinBank into secondaryBank. Clean and simple! ;))
Run the following command to build:
./gradlew clean build
and test:
java -jar build/libs/*.jar 11 1000000 EM1
Transfer 11000000 to EM1
VCB only support transfer amount less than 10M
Retry with VTB
VTB transfer success
Transfer Salary success
Now let’s swap VietcomBank to secondaryBank and VietinBank to primaryBank:
| |
Rebuilding and running gives:
Transfer 11000000 to EM1
VTB transfer success
Transfer Salary success
Just by changing the wiring configuration in IocdiApplication, the program’s runtime behavior is seamlessly altered without touching business logic!
Conclusion
IoC and DI may feel abstract to beginners, but mastering them is a crucial milestone. Most enterprise frameworks rely on these principles. Furthermore, writing Testable Classes is a mandatory prerequisite for Unit Testing, which will save you countless times throughout your career ;)). The time invested in mastering IoC and DI is worth every minute! :muscle:
The sample code is updated in this repo. Thanks for reading!