Skip to main content

D โ€” Dependency Inversion Principle

"High-level modules should not depend on low-level modules. Both should depend on abstractions." โ€” Robert C. Martin


๐Ÿง  What Does It Mean?โ€‹

Don't let your important business logic classes depend directly on concrete implementations (like a specific database driver, a specific email provider, etc.).

Instead, both the high-level class and low-level class should depend on an interface (abstraction).

Real-world analogy: When you plug a lamp into the wall, you don't care if the electricity comes from a coal plant, solar panels, or a nuclear reactor. The plug socket (interface) is the abstraction. Your lamp depends on the socket โ€” not on where the electricity comes from. You can swap the power source without touching the lamp.


๐ŸŽฏ Why Should I Care?โ€‹

Here's a story that plays out in real companies:

A startup builds their entire backend on AWS โ€” S3 for file storage, SES for email, DynamoDB for data, SNS for notifications. Every service directly imports and uses AWS SDK classes:

// Business logic tightly coupled to AWS
public class DocumentService {
private final AmazonS3 s3Client = AmazonS3ClientBuilder.defaultClient();
private final AmazonSES sesClient = AmazonSESClientBuilder.defaultClient();

public void processDocument(Document doc) {
s3Client.putObject("my-bucket", doc.getKey(), doc.getContent());
sesClient.sendEmail(buildNotification(doc));
}
}

Two years later, the company gets acquired. The new parent company mandates migration to Google Cloud Platform. Every service class that directly used AWS SDK must be rewritten โ€” that's 150+ classes. The migration takes 9 months and introduces dozens of regressions.

If they had applied DIP:

  • DocumentService depends on FileStorage and EmailSender interfaces
  • AWS implementations (S3FileStorage, SESEmailSender) and GCP implementations (GCSFileStorage, GmailEmailSender) both implement the same interfaces
  • Migration = swap the implementation beans. Zero business logic changes.

This is the cost of ignoring DIP:

  • ๐Ÿ”’ Vendor lock-in โ€” your business logic is married to a specific technology
  • ๐Ÿงช Untestable โ€” you can't unit test without a real database/cloud service
  • ๐Ÿข Painful migrations โ€” switching databases, cloud providers, or libraries is a rewrite
  • ๐Ÿงฉ Rigid architecture โ€” every infrastructure change cascades through business code

โŒ Bad Example โ€” Violating DIPโ€‹

// Low-level class โ€” a specific implementation
public class MySQLUserRepository {
public void save(String username) {
System.out.println("Saving to MySQL: " + username);
}
}
// High-level class depends DIRECTLY on the low-level MySQLUserRepository
public class UserService {

// Tightly coupled to MySQL! โŒ
private MySQLUserRepository repository = new MySQLUserRepository();

public void registerUser(String username) {
// some business logic...
repository.save(username);
}
}

Why is this bad?

  • Want to switch to PostgreSQL? You must modify UserService.
  • Want to write unit tests with a fake/mock repository? You can't โ€” it's hardcoded.
  • UserService (high-level business logic) is directly tied to MySQLUserRepository (low-level detail).

โœ… Good Example โ€” Applying DIPโ€‹

Introduce an interface (abstraction) that both the high-level and low-level modules depend on:

// The abstraction โ€” both sides depend on this
public interface UserRepository {
void save(String username);
}
// Low-level: MySQL implementation
public class MySQLUserRepository implements UserRepository {
@Override
public void save(String username) {
System.out.println("Saving to MySQL: " + username);
}
}
// Low-level: PostgreSQL implementation (easy to add!)
public class PostgreSQLUserRepository implements UserRepository {
@Override
public void save(String username) {
System.out.println("Saving to PostgreSQL: " + username);
}
}
// Low-level: In-memory implementation (great for testing!)
public class InMemoryUserRepository implements UserRepository {
private final List<String> users = new ArrayList<>();

@Override
public void save(String username) {
users.add(username);
System.out.println("Saved in memory: " + username);
}
}
// High-level: UserService now depends on the INTERFACE, not a concrete class
public class UserService {

private final UserRepository repository; // interface, not concrete class โœ…

// Dependency is INJECTED โ€” not created inside
public UserService(UserRepository repository) {
this.repository = repository;
}

public void registerUser(String username) {
// business logic...
repository.save(username);
}
}

Now you can plug in any repository without touching UserService!

// Easily swap implementations
UserService mysqlService = new UserService(new MySQLUserRepository());
UserService postgresService = new UserService(new PostgreSQLUserRepository());
UserService testService = new UserService(new InMemoryUserRepository());

๐Ÿ” How to Spot Violationsโ€‹

SmellWhat It Means
new ConcreteClass() in business logicHigh-level module directly creates low-level dependency
Import of infrastructure packagesBusiness class imports com.mysql, com.amazonaws, javax.mail
Can't test without external systemsUnit tests need a database, API key, or network connection
Class name in the field typeprivate MySQLRepo repo instead of private UserRepo repo
Configuration scattered in codeConnection strings, API keys hardcoded in business classes
"Works on my machine"Different environments need different implementations but code is rigid

The Dependency Direction Testโ€‹

Draw the dependency arrows in your code:

โŒ Without DIP:
UserService โ†’ MySQLUserRepository โ†’ MySQL Driver
(high-level depends on low-level โ€” dependency flows DOWN)

โœ… With DIP:
UserService โ†’ UserRepository (interface) โ† MySQLUserRepository
(both depend on the abstraction โ€” dependency is INVERTED)

The key insight: the interface is owned by the high-level module, not the low-level one. The business layer defines what it needs; the infrastructure layer implements it.


๐ŸŒฑ In a Spring Boot Applicationโ€‹

Spring's dependency injection is literally built on DIP. When you use @Autowired or constructor injection, Spring injects the right implementation at runtime.

// The interface (abstraction)
public interface PaymentGateway {
void charge(String customerId, double amount);
}
// Real implementation (used in production)
@Component
@Profile("production")
public class StripePaymentGateway implements PaymentGateway {
@Override
public void charge(String customerId, double amount) {
System.out.println("Charging via Stripe: $" + amount + " for customer " + customerId);
// real Stripe API call here
}
}
// Fake implementation (used in development/testing)
@Component
@Profile("development")
public class MockPaymentGateway implements PaymentGateway {
@Override
public void charge(String customerId, double amount) {
System.out.println("[MOCK] Fake charge of $" + amount + " for " + customerId);
}
}
@Service
public class OrderService {

private final PaymentGateway paymentGateway;

// Spring injects the right implementation based on @Profile
public OrderService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}

public void placeOrder(String customerId, double total) {
// business logic...
paymentGateway.charge(customerId, total);
}
}

In production โ†’ Stripe is injected. In testing โ†’ Mock is injected. OrderService never changes โ€” it always works with the PaymentGateway interface. โœ…


๐Ÿข Real-World Use Casesโ€‹

1. Multi-Cloud Portabilityโ€‹

A SaaS company needs to deploy on both AWS and Azure to serve different enterprise clients:

// Ports (interfaces owned by the domain)
public interface FileStorage {
void upload(String path, byte[] data);
byte[] download(String path);
void delete(String path);
}

public interface MessageQueue {
void publish(String topic, String message);
void subscribe(String topic, Consumer<String> handler);
}
// AWS Adapters
@Component @Profile("aws")
public class S3FileStorage implements FileStorage { /* AWS S3 SDK calls */ }

@Component @Profile("aws")
public class SQSMessageQueue implements MessageQueue { /* AWS SQS SDK calls */ }

// Azure Adapters
@Component @Profile("azure")
public class BlobFileStorage implements FileStorage { /* Azure Blob SDK calls */ }

@Component @Profile("azure")
public class ServiceBusMessageQueue implements MessageQueue { /* Azure Service Bus calls */ }

Switching cloud providers is a configuration change, not a code change:

# application-aws.yml
spring.profiles.active: aws

# application-azure.yml
spring.profiles.active: azure

2. Database Migrationโ€‹

A growing startup needs to migrate from MongoDB to PostgreSQL as their data model stabilizes:

// Domain defines what it needs
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(String id);
List<Order> findByCustomer(String customerId);
void deleteById(String id);
}

// Phase 1: MongoDB implementation (current)
@Repository @Profile("mongo")
public class MongoOrderRepository implements OrderRepository {
@Autowired private MongoTemplate mongoTemplate;
// MongoDB-specific implementation
}

// Phase 2: PostgreSQL implementation (migration target)
@Repository @Profile("postgres")
public class JpaOrderRepository implements OrderRepository {
@Autowired private JpaOrderJpaRepository jpaRepo;
// JPA-specific implementation
}

// Phase 3: Dual-write implementation (migration period)
@Repository @Profile("migration")
public class DualWriteOrderRepository implements OrderRepository {
private final MongoOrderRepository mongoRepo;
private final JpaOrderRepository jpaRepo;

@Override
public Order save(Order order) {
mongoRepo.save(order); // write to old
return jpaRepo.save(order); // write to new
}
// ... read from new, fallback to old
}

The business logic (OrderService) never changes throughout the entire migration.

3. A/B Testing Infrastructureโ€‹

A product team needs to test different recommendation algorithms:

public interface RecommendationEngine {
List<Product> recommend(User user, int count);
}

@Component("collaborative")
public class CollaborativeFilteringEngine implements RecommendationEngine {
// Based on similar users' behavior
}

@Component("content-based")
public class ContentBasedEngine implements RecommendationEngine {
// Based on product attributes
}

@Component("ml-model-v2")
public class MLModelV2Engine implements RecommendationEngine {
// New ML model being A/B tested
}

@Service
public class ProductPageService {
private final Map<String, RecommendationEngine> engines;

public List<Product> getRecommendations(User user) {
String variant = abTestService.getVariant(user, "recommendation-algo");
return engines.get(variant).recommend(user, 10);
}
}

New recommendation algorithms are deployed without touching the product page service.


๐Ÿ—๏ธ Architecture-Level Deep Diveโ€‹

Hexagonal Architecture (Ports and Adapters)โ€‹

DIP is the foundation of Hexagonal Architecture, one of the most important architectural patterns for enterprise applications:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
Driving Adapters โ”‚ โ”‚ Driven Adapters
โ”‚ โ”‚
[REST Controller] โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ [MySQL Repository]
[GraphQL API] โ”‚โ”€โ”€โ”€โ–ถโ”‚ DOMAIN โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”‚ [Redis Cache]
[CLI Command] โ”‚ โ”‚ (Core โ”‚ โ”‚ [Stripe Gateway]
[Event Listener] โ”‚ โ”‚ Business โ”‚ โ”‚ [S3 File Storage]
โ”‚ โ”‚ Logic) โ”‚ โ”‚ [Kafka Producer]
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚ โ–ฒ โ–ฒ โ”‚
โ”‚ Ports Ports โ”‚
โ”‚ (Input) (Output) โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Key rules:

  1. The domain (center) defines interfaces (ports) โ€” it has ZERO external dependencies
  2. Adapters (outer ring) implement the ports โ€” they depend on the domain
  3. Dependencies always point inward (toward the domain)
  4. The domain is pure business logic โ€” no frameworks, no databases, no HTTP
// Port (defined by domain โ€” the domain OWNS this interface)
package com.app.domain.port;

public interface OrderPort {
Order save(Order order);
Optional<Order> findById(OrderId id);
}
// Adapter (implements the port โ€” infrastructure depends on domain)
package com.app.infrastructure.persistence;

@Repository
public class JpaOrderAdapter implements OrderPort {
@Autowired private JpaOrderRepository jpaRepo;

@Override
public Order save(Order order) {
OrderEntity entity = OrderMapper.toEntity(order);
return OrderMapper.toDomain(jpaRepo.save(entity));
}
}

Dependency Graphs and the Acyclic Dependency Principleโ€‹

DIP helps avoid circular dependencies โ€” a major source of architectural rot:

โŒ Circular dependency (compilation/deployment nightmare)
OrderService โ†’ PaymentService โ†’ NotificationService โ†’ OrderService

โœ… DIP breaks the cycle
OrderService โ†’ PaymentPort (interface) โ† PaymentService
โ†“
NotificationPort (interface) โ† NotificationService

By depending on interfaces rather than concrete classes, you naturally break cycles and create a Directed Acyclic Graph (DAG) of dependencies.

DIP in Java Module System (JPMS)โ€‹

Java 9+ modules enforce DIP at the package level:

// Domain module โ€” defines ports, has NO infrastructure dependencies
module com.app.domain {
exports com.app.domain.model;
exports com.app.domain.port;
// NOTE: does NOT require any infrastructure modules
}

// Infrastructure module โ€” implements ports
module com.app.infrastructure.mysql {
requires com.app.domain; // depends on domain
requires java.sql; // infrastructure detail

provides com.app.domain.port.UserRepository
with com.app.infrastructure.mysql.MySQLUserRepository;
}

// Application module โ€” wires everything together
module com.app.application {
requires com.app.domain;
requires com.app.infrastructure.mysql;
uses com.app.domain.port.UserRepository;
}

DIP and Clean Architecture Layersโ€‹

Robert C. Martin's Clean Architecture is built entirely on DIP:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Frameworks & Drivers โ”‚ โ† Spring, MySQL, S3
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚ โ”‚ Interface Adapters โ”‚ โ”‚ โ† Controllers, Gateways
โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ Application Business โ”‚ โ”‚ โ”‚ โ† Use Cases
โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ โ”‚ โ”‚ Enterprise Business Rules โ”‚ โ”‚ โ”‚ โ”‚ โ† Entities, Domain
โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚
โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Dependency Rule: Dependencies ALWAYS point inward.
Inner layers NEVER know about outer layers.
This is DIP applied at every boundary.

IoC Containers Beyond Springโ€‹

DIP is a principle โ€” dependency injection is a mechanism. Different ecosystems implement it differently:

FrameworkDI Mechanism
Spring@Autowired, @Component, @Profile
Jakarta CDI@Inject, @Produces, @Alternative
Guice@Inject, Module bindings
Dagger@Inject, @Component, compile-time DI
ManualConstructor injection without any framework

You don't need a framework to apply DIP โ€” it's about design, not tooling:

// Pure DIP โ€” no framework needed
public class Application {
public static void main(String[] args) {
UserRepository repo = new MySQLUserRepository(dataSource);
EmailService email = new SmtpEmailService(smtpConfig);
UserService service = new UserService(repo, email);
// Dependencies are inverted and injected manually
}
}

โš–๏ธ Trade-offs & When NOT to Applyโ€‹

When Abstraction Is Overheadโ€‹

Not every class needs an interface. Creating abstractions for single, stable implementations adds indirection with no benefit:

// Over-engineered โ€” this will only ever have one implementation
public interface StringUtils {
String capitalize(String input);
String truncate(String input, int maxLength);
}

public class StringUtilsImpl implements StringUtils {
// ... the only implementation, forever
}

// Just use a utility class directly
public final class StringUtils {
public static String capitalize(String input) { /* ... */ }
public static String truncate(String input, int maxLength) { /* ... */ }
}

Decision Framework: When to Abstractโ€‹

QuestionIf Yes โ†’ AbstractIf No โ†’ Direct
Will there be multiple implementations?โœ…
Do you need to mock this in tests?โœ…
Is it an external system (DB, API, file system)?โœ…
Could the technology change?โœ…
Is it pure business logic with no side effects?โœ…
Is it a utility/helper class?โœ…
Is it framework-specific glue code?โœ…

The Cost of Interface Proliferationโ€‹

Every interface adds:

  • A file to navigate โ€” IDE has one more type to search through
  • Indirection โ€” debugging requires one more "Go to Implementation" click
  • Naming burden โ€” UserService vs UserServiceImpl is a known anti-pattern

Anti-pattern alert: If every class in your project has a corresponding interface with Impl suffix, you're probably over-abstracting:

// Meaningless abstraction โ€” 1:1 interface-to-impl ratio
public interface UserService { /* ... */ }
public class UserServiceImpl implements UserService { /* ... */ }

// Better: only create interfaces where multiple implementations exist or are expected
public class UserService {
private final UserRepository repository; // THIS has multiple impls โ€” worth abstracting
// UserService itself is a concrete class โ€” fine!
}

Pragmatic DIPโ€‹

Apply DIP at architectural boundaries where the cost of coupling is highest:

Apply DIP here (high-value boundaries):
โ”œโ”€โ”€ Database access โ†’ Repository interfaces
โ”œโ”€โ”€ External APIs โ†’ Gateway/Client interfaces
โ”œโ”€โ”€ File storage โ†’ Storage interfaces
โ”œโ”€โ”€ Message queues โ†’ Publisher/Subscriber interfaces
โ””โ”€โ”€ Third-party services โ†’ Adapter interfaces

Skip DIP here (low-value, internal):
โ”œโ”€โ”€ Utility classes โ†’ Use directly
โ”œโ”€โ”€ DTOs and value objects โ†’ Use directly
โ”œโ”€โ”€ Framework config โ†’ Use directly
โ””โ”€โ”€ Internal helpers โ†’ Use directly

๐Ÿงช Testing Implicationsโ€‹

The Testing Trinity: Fake vs Mock vs Stubโ€‹

DIP enables three types of test doubles, each serving a different purpose:

TypePurposeExample
StubReturns pre-configured valueswhen(repo.findById(1L)).thenReturn(user)
MockVerifies interactions occurredverify(emailService).sendWelcomeEmail(email)
FakeWorking lightweight implementationInMemoryUserRepository with a HashMap
// Fake โ€” the most powerful test double for DIP
public class InMemoryUserRepository implements UserRepository {
private final Map<Long, User> store = new HashMap<>();
private long sequence = 0;

@Override
public User save(User user) {
user.setId(++sequence);
store.put(user.getId(), user);
return user;
}

@Override
public Optional<User> findById(Long id) {
return Optional.ofNullable(store.get(id));
}

@Override
public List<User> findAll() {
return new ArrayList<>(store.values());
}

@Override
public void deleteById(Long id) {
store.remove(id);
}
}

Testing at Architectural Boundariesโ€‹

// Unit test โ€” uses fakes, fast, no Spring context
class UserServiceTest {
private final UserRepository repo = new InMemoryUserRepository();
private final EmailService email = new NoOpEmailService();
private final UserService service = new UserService(repo, email);

@Test
void shouldRegisterUser() {
service.registerUser("john", "[email protected]");
assertThat(repo.findAll()).hasSize(1);
}
}

// Integration test โ€” verifies real adapter works correctly
@DataJpaTest
class JpaUserRepositoryIntegrationTest {
@Autowired private JpaUserRepository repository;

@Test
void shouldPersistAndRetrieveUser() {
User saved = repository.save(new User("john"));
assertThat(repository.findById(saved.getId())).isPresent();
}
}

// Contract test โ€” ensures ALL implementations behave the same
abstract class UserRepositoryContractTest {
abstract UserRepository createRepository();

@Test
void saveShouldAssignId() {
User user = createRepository().save(new User("john"));
assertThat(user.getId()).isNotNull();
}

@Test
void findByIdShouldReturnSavedUser() {
UserRepository repo = createRepository();
User saved = repo.save(new User("john"));
assertThat(repo.findById(saved.getId())).contains(saved);
}
}

Test Speed Impactโ€‹

ApproachTest SpeedWhy
Direct DB dependency~200ms/testReal database connection per test
Mocked dependency~5ms/testNo I/O, but fragile to refactoring
Fake implementation~1ms/testIn-memory, realistic behavior, refactor-safe

A suite of 500 tests:

  • Without DIP (real DB): 100 seconds ๐Ÿข
  • With DIP (fakes): 0.5 seconds ๐Ÿš€

๐Ÿ”— Relationship to Other SOLID Principlesโ€‹

PrincipleHow It Connects to DIP
Single Responsibility (SRP)SRP creates focused classes that are natural candidates for abstraction behind interfaces
Open/Closed (OCP)DIP is the mechanism that enables OCP โ€” new implementations extend behavior without modifying existing code
Liskov Substitution (LSP)DIP makes substitution possible; LSP ensures it's safe โ€” both are needed for reliable polymorphism
Interface Segregation (ISP)ISP ensures the interfaces that DIP depends on are small and focused, not bloated

DIP is the architectural enabler. While the other four principles guide class-level design, DIP shapes how entire modules and systems are structured. It's the principle that makes your architecture pluggable, testable, and future-proof.

The SOLID synergy chain:

SRP โ†’ focused classes
ISP โ†’ focused interfaces for those classes
DIP โ†’ depend on those focused interfaces
OCP โ†’ extend by adding new implementations
LSP โ†’ new implementations are safe substitutions

๐Ÿ”‘ Key Conceptsโ€‹

TermMeaning
High-level moduleYour business logic (OrderService, UserService)
Low-level moduleThe detail (MySQLRepo, StripeGateway, EmailSender)
AbstractionThe interface that sits between them
Dependency InjectionProviding the dependency from outside (constructor injection)
Inversion of Control (IoC)The framework controls object lifecycle, not your code
PortAn interface defined by the domain (hexagonal architecture)
AdapterAn implementation of a port for a specific technology

๐Ÿ’ก Quick Rule of Thumbโ€‹

If you see new ConcreteClass() inside a service or business class, ask yourself: "Should this be an interface instead?"

A high-level class should never call new on a low-level class. Let Spring (or another IoC container) manage that.


๐Ÿ“Œ Summaryโ€‹

BadGood
Dependency onConcrete class (MySQLUserRepository)Interface (UserRepository)
How creatednew MySQLUserRepository() inside serviceInjected via constructor
Swap implementationMust modify business classJust provide a different bean
TestabilityHard โ€” can't mockEasy โ€” inject a test double

You've completed all 5 SOLID principles! See the full summary โ†’


Interview Questionsโ€‹

Q: How is DIP different from dependency injection?โ€‹

A: DIP is a design principle about dependency direction toward abstractions. Dependency injection is a technique to provide those abstractions at runtime.

Q: In a Spring application, who should own repository interfaces?โ€‹

A: The domain/application layer should own the interface contract. Infrastructure modules implement it. This preserves business-level control over required behavior.

Q: What is the risk of putting framework-specific types in domain interfaces?โ€‹

A: It leaks infrastructure concerns into core business logic, making tests and future migrations harder.

Q: How would you apply DIP to external integrations like payment and notification providers?โ€‹

A: Define provider-agnostic ports (interfaces), implement adapters per vendor, and wire adapter selection via configuration/profile.

Q: What interview answer shows mature DIP usage in testing?โ€‹

A: Use contract-focused fakes for domain tests and thin mocks only at boundaries. This validates behavior without over-coupling tests to implementation details.

Q: When can DIP be overused?โ€‹

A: Creating interfaces for classes with a single stable implementation and no testing pressure can add unnecessary indirection.

Q: How do you migrate from hardcoded dependencies to DIP safely?โ€‹

A: Introduce an interface around the existing concrete class, inject it through constructors, update call sites incrementally, and keep behavior unchanged with regression tests.

Q: What does good DIP look like in incident response?โ€‹

A: You can quickly replace failing adapters (for example, switch to fallback provider) without changing core business services.