D (Dependency Inversion) from SOLID
The Dependency Inversion Principle: Stop Depending on Details
What Is Dependency Inversion, Really?
Let me ask you a question: when you plug your phone into a charger, does your phone know whether it’s connected to a wall adapter, a laptop USB port, or a portable battery pack? Nope. Your phone just knows it’s receiving power through a USB port. It doesn’t care about the specifics of where that power comes from.
That’s Dependency Inversion Principle (DIP) in a nutshell.
The formal definition is: High-level modules should not depend on low-level modules. Both should depend on abstractions. Furthermore, abstractions should not depend on details. Details should depend on abstractions.
In plain English: Your important business logic shouldn’t be tightly coupled to nitty-gritty implementation details. Instead, both should communicate through interfaces or abstract contracts. This way, you can swap out the details without touching your core logic.
Here’s the mind-bending part: it’s called “inversion” because we’re inverting the traditional dependency structure. Normally, high-level code depends directly on low-level code (business logic → database implementation). With DIP, we flip it: both depend on abstractions (business logic → interface ← database implementation).
Why Should You Care About DIP?
Let me tell you a war story. I once worked on an e-commerce system where the business logic was directly using MySQL-specific queries. Everything worked great—until the client decided they wanted to switch to PostgreSQL for better performance.
We had to rewrite hundreds of classes. The business logic, the inventory management, the order processing—everything had MySQL queries embedded in it. It took three months and introduced dozens of bugs.
Know what the worst part was? The actual database switch took two hours. The other three months were spent untangling business logic from database details.
That’s what happens when you violate DIP. Here’s why it matters:
1. Makes Code Testable
You can test business logic without needing a real database, email server, or payment gateway.
2. Enables Flexibility
Swap implementations without touching core logic. Move from MySQL to MongoDB? Easy. Switch email providers? No problem.
3. Reduces Coupling
High-level code doesn’t need to know about low-level implementation details.
4. Improves Maintainability
Changes to implementation details don’t ripple through your entire codebase.
5. Enables Parallel Development
Teams can work on interfaces first, then implementations separately.
A Simple Example: Email Notifications
The Wrong Way: Direct Dependencies
Let’s start with a common scenario—sending email notifications:
public class OrderService {
public void placeOrder(Order order) {
// Validate order
if (order.getTotal() <= 0) {
throw new IllegalArgumentException("Invalid order total");
}
// Save to database - TIGHTLY COUPLED!
MySQLDatabase database = new MySQLDatabase();
database.connect("jdbc:mysql://localhost:3306/shop");
database.execute("INSERT INTO orders VALUES (" +
order.getId() + ", '" +
order.getCustomerEmail() + "', " +
order.getTotal() + ")");
database.disconnect();
// Send email - TIGHTLY COUPLED!
SmtpEmailSender emailSender = new SmtpEmailSender();
emailSender.setHost("smtp.gmail.com");
emailSender.setPort(587);
emailSender.setUsername("[email protected]");
emailSender.setPassword("supersecret");
emailSender.connect();
emailSender.send(
order.getCustomerEmail(),
"Order Confirmation",
"Your order #" + order.getId() + " has been placed"
);
emailSender.disconnect();
// Log the order - TIGHTLY COUPLED!
FileLogger logger = new FileLogger();
logger.setFilePath("/var/log/orders.log");
logger.write("Order placed: " + order.getId());
logger.close();
}
}
Look at all those concrete dependencies! OrderService knows:
- The specific database (MySQL)
- How to connect to it
- The email protocol (SMTP)
- Email server configuration
- How logging works
- Where log files are stored
Want to test this? Good luck! You need a real MySQL database, an SMTP server, and file system access. Want to switch to PostgreSQL? You’re rewriting OrderService. Want to use SendGrid instead of SMTP? More rewrites.
This is a nightmare.
The Right Way: Depend on Abstractions
Let’s invert those dependencies:
// Define abstractions (interfaces) for what we need
public interface OrderRepository {
void save(Order order);
Order findById(Long id);
}
public interface NotificationService {
void sendOrderConfirmation(Order order);
}
public interface Logger {
void log(String message);
}
// Now our OrderService depends on abstractions
public class OrderService {
private final OrderRepository orderRepository;
private final NotificationService notificationService;
private final Logger logger;
// Dependencies injected through constructor
public OrderService(
OrderRepository orderRepository,
NotificationService notificationService,
Logger logger
) {
this.orderRepository = orderRepository;
this.notificationService = notificationService;
this.logger = logger;
}
public void placeOrder(Order order) {
// Validate order
if (order.getTotal() <= 0) {
throw new IllegalArgumentException("Invalid order total");
}
// Save to database - using abstraction!
orderRepository.save(order);
// Send notification - using abstraction!
notificationService.sendOrderConfirmation(order);
// Log the order - using abstraction!
logger.log("Order placed: " + order.getId());
}
}
Now OrderService doesn’t know or care about:
- Which database is used
- How emails are sent
- Where logs are written
It just depends on interfaces. Clean, simple, flexible.
Now let’s implement those interfaces:
// MySQL implementation
public class MySQLOrderRepository implements OrderRepository {
private final DataSource dataSource;
public MySQLOrderRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void save(Order order) {
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO orders (id, customer_email, total) VALUES (?, ?, ?)"
)) {
stmt.setLong(1, order.getId());
stmt.setString(2, order.getCustomerEmail());
stmt.setDouble(3, order.getTotal());
stmt.executeUpdate();
} catch (SQLException e) {
throw new DataAccessException("Failed to save order", e);
}
}
@Override
public Order findById(Long id) {
// Implementation...
return null;
}
}
// Email notification implementation
public class EmailNotificationService implements NotificationService {
private final EmailSender emailSender;
public EmailNotificationService(EmailSender emailSender) {
this.emailSender = emailSender;
}
@Override
public void sendOrderConfirmation(Order order) {
String subject = "Order Confirmation #" + order.getId();
String body = buildEmailBody(order);
emailSender.send(
order.getCustomerEmail(),
subject,
body
);
}
private String buildEmailBody(Order order) {
return "Thank you for your order!\n\n" +
"Order ID: " + order.getId() + "\n" +
"Total: $" + order.getTotal();
}
}
// File-based logger implementation
public class FileLogger implements Logger {
private final String logFilePath;
public FileLogger(String logFilePath) {
this.logFilePath = logFilePath;
}
@Override
public void log(String message) {
try (FileWriter writer = new FileWriter(logFilePath, true)) {
String timestamp = LocalDateTime.now().toString();
writer.write("[" + timestamp + "] " + message + "\n");
} catch (IOException e) {
// Handle error
System.err.println("Failed to write log: " + e.getMessage());
}
}
}
Now look what we can do:
// Production configuration
public class ProductionConfig {
public OrderService createOrderService() {
DataSource dataSource = createProductionDataSource();
OrderRepository orderRepository = new MySQLOrderRepository(dataSource);
EmailSender emailSender = new SmtpEmailSender(/* config */);
NotificationService notificationService = new EmailNotificationService(emailSender);
Logger logger = new FileLogger("/var/log/orders.log");
return new OrderService(orderRepository, notificationService, logger);
}
}
// Test configuration
public class TestConfig {
public OrderService createOrderService() {
OrderRepository orderRepository = new InMemoryOrderRepository();
NotificationService notificationService = new MockNotificationService();
Logger logger = new ConsoleLogger();
return new OrderService(orderRepository, notificationService, logger);
}
}
// Want to switch to PostgreSQL? Just change one line:
OrderRepository orderRepository = new PostgreSQLOrderRepository(dataSource);
// Want to use SendGrid for emails? Easy:
EmailSender emailSender = new SendGridEmailSender(apiKey);
NotificationService notificationService = new EmailNotificationService(emailSender);
// Want to log to a centralized logging service? Simple:
Logger logger = new CloudLogger(loggingServiceUrl);
See the power? We can swap any implementation without touching OrderService.
A Real-World Example: Notification System
Let me show you a more complex example—a notification system that can send messages via email, SMS, push notifications, or Slack.
The Wrong Way: Hardcoded Dependencies
public class UserNotifier {
public void notifyUser(User user, String message, String type) {
if (type.equals("email")) {
// Email logic embedded here
SmtpClient smtp = new SmtpClient();
smtp.connect("smtp.gmail.com", 587);
smtp.authenticate("[email protected]", "password");
smtp.send(user.getEmail(), "Notification", message);
smtp.disconnect();
} else if (type.equals("sms")) {
// SMS logic embedded here
TwilioClient twilio = new TwilioClient("account_sid", "auth_token");
twilio.sendSms(user.getPhoneNumber(), message);
} else if (type.equals("push")) {
// Push notification logic embedded here
FirebaseClient firebase = new FirebaseClient("api_key");
firebase.sendPushNotification(user.getDeviceToken(), message);
} else if (type.equals("slack")) {
// Slack logic embedded here
SlackClient slack = new SlackClient("webhook_url");
slack.postMessage(user.getSlackUsername(), message);
}
}
}
This is terrible:
- Adding a new notification type requires changing
UserNotifier - Testing requires all external services
- Configuration is hardcoded
- It violates Open/Closed Principle too!
The Right Way: Abstraction and Strategy Pattern
// Define the abstraction
public interface NotificationChannel {
void send(User user, String message);
String getChannelName();
boolean supports(User user);
}
// Email implementation
public class EmailNotificationChannel implements NotificationChannel {
private final EmailService emailService;
public EmailNotificationChannel(EmailService emailService) {
this.emailService = emailService;
}
@Override
public void send(User user, String message) {
if (!supports(user)) {
throw new IllegalArgumentException("User has no email address");
}
String subject = "Notification from " + System.getProperty("app.name");
emailService.send(user.getEmail(), subject, message);
}
@Override
public String getChannelName() {
return "EMAIL";
}
@Override
public boolean supports(User user) {
return user.getEmail() != null && !user.getEmail().isEmpty();
}
}
// SMS implementation
public class SmsNotificationChannel implements NotificationChannel {
private final SmsService smsService;
public SmsNotificationChannel(SmsService smsService) {
this.smsService = smsService;
}
@Override
public void send(User user, String message) {
if (!supports(user)) {
throw new IllegalArgumentException("User has no phone number");
}
smsService.sendSms(user.getPhoneNumber(), message);
}
@Override
public String getChannelName() {
return "SMS";
}
@Override
public boolean supports(User user) {
return user.getPhoneNumber() != null && !user.getPhoneNumber().isEmpty();
}
}
// Push notification implementation
public class PushNotificationChannel implements NotificationChannel {
private final PushNotificationService pushService;
public PushNotificationChannel(PushNotificationService pushService) {
this.pushService = pushService;
}
@Override
public void send(User user, String message) {
if (!supports(user)) {
throw new IllegalArgumentException("User has no device token");
}
pushService.sendPush(user.getDeviceToken(), "Notification", message);
}
@Override
public String getChannelName() {
return "PUSH";
}
@Override
public boolean supports(User user) {
return user.getDeviceToken() != null && !user.getDeviceToken().isEmpty();
}
}
// Slack implementation
public class SlackNotificationChannel implements NotificationChannel {
private final SlackService slackService;
public SlackNotificationChannel(SlackService slackService) {
this.slackService = slackService;
}
@Override
public void send(User user, String message) {
if (!supports(user)) {
throw new IllegalArgumentException("User has no Slack username");
}
slackService.postMessage(user.getSlackUsername(), message);
}
@Override
public String getChannelName() {
return "SLACK";
}
@Override
public boolean supports(User user) {
return user.getSlackUsername() != null && !user.getSlackUsername().isEmpty();
}
}
// High-level notification service
public class UserNotificationService {
private final List<NotificationChannel> channels;
private final NotificationPreferenceRepository preferenceRepository;
public UserNotificationService(
List<NotificationChannel> channels,
NotificationPreferenceRepository preferenceRepository
) {
this.channels = channels;
this.preferenceRepository = preferenceRepository;
}
public void notifyUser(User user, String message) {
NotificationPreference preference = preferenceRepository.findByUser(user);
for (String preferredChannel : preference.getPreferredChannels()) {
channels.stream()
.filter(channel -> channel.getChannelName().equals(preferredChannel))
.filter(channel -> channel.supports(user))
.findFirst()
.ifPresent(channel -> {
try {
channel.send(user, message);
} catch (Exception e) {
// Log error and try next channel
System.err.println("Failed to send via " + channel.getChannelName());
}
});
}
}
public void notifyUserViaChannel(User user, String message, String channelName) {
channels.stream()
.filter(channel -> channel.getChannelName().equals(channelName))
.filter(channel -> channel.supports(user))
.findFirst()
.ifPresent(channel -> channel.send(user, message));
}
public List<String> getAvailableChannels(User user) {
return channels.stream()
.filter(channel -> channel.supports(user))
.map(NotificationChannel::getChannelName)
.collect(Collectors.toList());
}
}
Now look at the flexibility:
// Production setup
public class NotificationConfig {
public UserNotificationService createNotificationService() {
List<NotificationChannel> channels = new ArrayList<>();
// Email channel
EmailService emailService = new SmtpEmailService(/* config */);
channels.add(new EmailNotificationChannel(emailService));
// SMS channel
SmsService smsService = new TwilioSmsService(/* config */);
channels.add(new SmsNotificationChannel(smsService));
// Push notification channel
PushNotificationService pushService = new FirebasePushService(/* config */);
channels.add(new PushNotificationChannel(pushService));
// Slack channel
SlackService slackService = new SlackWebhookService(/* config */);
channels.add(new SlackNotificationChannel(slackService));
NotificationPreferenceRepository preferenceRepository =
new DatabaseNotificationPreferenceRepository();
return new UserNotificationService(channels, preferenceRepository);
}
}
// Test setup - no external services needed!
public class TestNotificationConfig {
public UserNotificationService createNotificationService() {
List<NotificationChannel> channels = new ArrayList<>();
// Mock implementations
channels.add(new EmailNotificationChannel(new MockEmailService()));
channels.add(new SmsNotificationChannel(new MockSmsService()));
NotificationPreferenceRepository preferenceRepository =
new InMemoryNotificationPreferenceRepository();
return new UserNotificationService(channels, preferenceRepository);
}
}
// Want to add a new channel? Just implement the interface!
public class WhatsAppNotificationChannel implements NotificationChannel {
private final WhatsAppService whatsAppService;
public WhatsAppNotificationChannel(WhatsAppService whatsAppService) {
this.whatsAppService = whatsAppService;
}
@Override
public void send(User user, String message) {
whatsAppService.sendMessage(user.getWhatsAppNumber(), message);
}
@Override
public String getChannelName() {
return "WHATSAPP";
}
@Override
public boolean supports(User user) {
return user.getWhatsAppNumber() != null;
}
}
// Add to configuration:
channels.add(new WhatsAppNotificationChannel(new WhatsAppApiService(config)));
UserNotificationService never changes! We just add implementations.
DIP and Dependency Injection Frameworks
DIP works beautifully with dependency injection frameworks like Spring:
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final NotificationService notificationService;
private final PaymentGateway paymentGateway;
// Spring automatically injects dependencies
@Autowired
public OrderService(
OrderRepository orderRepository,
NotificationService notificationService,
PaymentGateway paymentGateway
) {
this.orderRepository = orderRepository;
this.notificationService = notificationService;
this.paymentGateway = paymentGateway;
}
public void placeOrder(Order order) {
// High-level business logic
paymentGateway.charge(order.getPaymentDetails(), order.getTotal());
orderRepository.save(order);
notificationService.sendOrderConfirmation(order);
}
}
// Configuration
@Configuration
public class AppConfig {
@Bean
public OrderRepository orderRepository(DataSource dataSource) {
return new JpaOrderRepository(dataSource);
}
@Bean
public NotificationService notificationService() {
return new EmailNotificationService(emailSender());
}
@Bean
public PaymentGateway paymentGateway() {
return new StripePaymentGateway(stripeApiKey);
}
}
// Test configuration
@TestConfiguration
public class TestConfig {
@Bean
@Primary // Override production beans in tests
public OrderRepository orderRepository() {
return new InMemoryOrderRepository();
}
@Bean
@Primary
public NotificationService notificationService() {
return new MockNotificationService();
}
@Bean
@Primary
public PaymentGateway paymentGateway() {
return new MockPaymentGateway();
}
}
Testing becomes trivial:
@SpringBootTest
public class OrderServiceTest {
@Autowired
private OrderService orderService;
@Autowired
private MockNotificationService notificationService;
@Test
public void testPlaceOrder() {
Order order = new Order(/* ... */);
orderService.placeOrder(order);
// Verify notification was sent
assertTrue(notificationService.wasNotificationSent(order));
}
}
The Dependency Inversion vs Dependency Injection
People often confuse these:
- Dependency Inversion Principle (DIP): A design principle about depending on abstractions, not concrete implementations
- Dependency Injection (DI): A technique for providing dependencies from the outside
DI is one way to achieve DIP, but not the only way:
// Dependency Injection (constructor injection)
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = repository; // Injected from outside
}
}
// Service Locator (another way to achieve DIP)
public class OrderService {
private final OrderRepository repository;
public OrderService() {
this.repository = ServiceLocator.get(OrderRepository.class);
}
}
// Factory (yet another way)
public class OrderService {
private final OrderRepository repository;
public OrderService() {
this.repository = RepositoryFactory.createOrderRepository();
}
}
Constructor injection (DI) is usually the best approach—it’s explicit, testable, and makes dependencies obvious.
Common DIP Violations and Fixes
Violation 1: New Keyword Everywhere
// WRONG - Creating concrete dependencies
public class ReportGenerator {
public void generateReport() {
DatabaseConnection db = new MySQLConnection(); // Concrete!
PdfGenerator pdf = new ApachePdfGenerator(); // Concrete!
EmailSender email = new SmtpEmailSender(); // Concrete!
// Use them...
}
}
// RIGHT - Depend on abstractions
public class ReportGenerator {
private final DatabaseConnection db;
private final PdfGenerator pdf;
private final EmailSender email;
public ReportGenerator(DatabaseConnection db, PdfGenerator pdf, EmailSender email) {
this.db = db;
this.pdf = pdf;
this.email = email;
}
}
Violation 2: Static Method Dependencies
// WRONG - Static coupling
public class OrderProcessor {
public void process(Order order) {
MySQLDatabase.save(order); // Static coupling!
SmtpEmail.send(order.getCustomerEmail(), "Order confirmed"); // Static!
}
}
// RIGHT - Injectable dependencies
public class OrderProcessor {
private final OrderRepository repository;
private final EmailService emailService;
public OrderProcessor(OrderRepository repository, EmailService emailService) {
this.repository = repository;
this.emailService = emailService;
}
public void process(Order order) {
repository.save(order);
emailService.send(order.getCustomerEmail(), "Order confirmed");
}
}
Violation 3: Leaky Abstractions
// WRONG - Abstraction exposes implementation details
public interface UserRepository {
void saveToMongoDB(User user); // MongoDB leaks through!
User findByMongoId(String id); // MongoDB leaks through!
}
// RIGHT - Pure abstraction
public interface UserRepository {
void save(User user);
Optional<User> findById(String id);
}
Practical Tips for Applying DIP
1. Extract Interfaces from Concrete Classes
Start with what you have and extract interfaces:
// Before
public class FileStorage {
public void save(String filename, byte[] data) { /* ... */ }
}
// After - extract interface
public interface Storage {
void save(String filename, byte[] data);
}
public class FileStorage implements Storage {
public void save(String filename, byte[] data) { /* ... */ }
}
// Now you can add implementations
public class CloudStorage implements Storage { /* ... */ }
public class DatabaseStorage implements Storage { /* ... */ }
2. Use Constructor Injection
Make dependencies explicit:
// Good
public class UserService {
private final UserRepository repository;
private final EmailService emailService;
public UserService(UserRepository repository, EmailService emailService) {
this.repository = repository;
this.emailService = emailService;
}
}
3. Program to Interfaces, Not Implementations
// Bad
MySQLUserRepository repository = new MySQLUserRepository();
// Good
UserRepository repository = new MySQLUserRepository();
4. Keep Abstractions Focused
Don’t let implementation details leak into interfaces:
// Bad - leaky abstraction
public interface DataStore {
ResultSet executeQuery(String sql); // SQL leaks through!
}
// Good - clean abstraction
public interface DataStore {
List<User> findUsers(UserCriteria criteria);
}
5. Test Through Abstractions
@Test
public void testOrderService() {
// Use test doubles (mocks/stubs)
OrderRepository mockRepository = mock(OrderRepository.class);
NotificationService mockNotification = mock(NotificationService.class);
OrderService service = new OrderService(mockRepository, mockNotification);
// Test without real dependencies
service.placeOrder(testOrder);
verify(mockRepository).save(testOrder);
verify(mockNotification).sendOrderConfirmation(testOrder);
}
When to Violate DIP
Case 1: Stable Dependencies
It’s OK to depend directly on very stable libraries:
// OK - String is stable
public class UserService {
public String formatUserName(User user) {
return user.getFirstName() + " " + user.getLastName();
}
}
// OK - Java collections are stable
public class OrderService {
public List<Order> getOrders() {
return new ArrayList<>();
}
}
Case 2: Simple Utility Classes
// OK for simple utilities
public class PriceCalculator {
public double calculate(double price) {
return price * Math.pow(1.1, 2); // Math is fine
}
}
Case 3: Value Objects
// OK - value objects don't need abstraction
public class OrderService {
public Order createOrder(Money price, Customer customer) {
return new Order(price, customer);
}
}
DIP and Other SOLID Principles
DIP works with the other principles:
- Single Responsibility: Each abstraction has one reason to change
- Open/Closed: Add new implementations without changing high-level code
- Liskov Substitution: Implementations must be proper substitutes
- Interface Segregation: Keep abstractions focused
Together, they create maintainable systems.
Conclusion
The Dependency Inversion Principle is about controlling which direction dependencies flow. Instead of high-level business logic depending on low-level implementation details, both depend on abstractions.
Think of abstractions as contracts. Your business logic says “I need something that can store orders” (the contract). You can then plug in any implementation that fulfills that contract—MySQL, PostgreSQL, MongoDB, an in-memory store for testing, whatever.
When you follow DIP:
- Your code becomes testable without external dependencies
- You can swap implementations easily
- Changes to details don’t affect business logic
- Your architecture becomes flexible and maintainable
When you violate DIP:
- Your business logic is tightly coupled to implementation details
- Testing requires full infrastructure
- Changing implementations requires rewriting business logic
- Your architecture becomes rigid and fragile
Remember: Depend on abstractions, not concretions. Your business logic is precious—don’t pollute it with implementation details. Let the details depend on the abstractions, not the other way around.
The key insight: It’s not just about creating interfaces—it’s about inverting the traditional dependency flow so that details (low-level modules) depend on business rules (high-level modules) through shared abstractions. That’s the “inversion” in Dependency Inversion Principle.
Continue reading
Next article
I (Interface Segregation) from SOLID
Related Content
S (Single Responsibility) from SOLID
Understand the Single Responsibility Principle from SOLID. Learn why a class should have only one reason to change and how SRP leads to more maintainable, testable code.
L (Liskov Substitution) from SOLID
Understand the Liskov Substitution Principle from SOLID. Learn why subclasses must be substitutable for their parent classes and how violations lead to subtle bugs in inheritance hierarchies.
I (Interface Segregation) from SOLID
Learn the Interface Segregation Principle from SOLID. Discover why fat interfaces force unnecessary dependencies and how to design focused, client-specific interfaces.