• Shift Elevate
  • Posts
  • Circular Dependencies: Dependency Inversion Refactoring | Clean Code

Circular Dependencies: Dependency Inversion Refactoring | Clean Code

Circular Dependencies create a web of interconnected classes that make code rigid and hard to test.

We will see how we can refactor using the Dependency Inversion technique.

Clean Code Reference

⚠️ Code Smell: Circular Dependencies
Refactoring: Dependency Inversion
🎯 Goal: Classes depend on abstractions, not concrete implementations

The Circular Dependencies code smell occurs when two or more classes directly depend on each other, creating a tight coupling that makes the code difficult to modify, test, and maintain. The Dependency Inversion refactoring technique introduces abstractions (interfaces) to break these circular relationships and establish a clear dependency hierarchy.

The Code Smell: Circular Dependencies

Circular Dependencies create a complex web of interconnected classes where each class directly depends on the other, making the codebase rigid and difficult to modify. This smell violates the Dependency Inversion Principle and creates tight coupling that prevents independent testing and modification of individual components.

Symptoms

Impact

Class A imports Class B, Class B imports Class A

Creates tight coupling between classes

Compilation errors when trying to modify one class

Makes code changes difficult and risky

Unit tests become complex and interdependent

Reduces testability and increases maintenance cost

Changes in one class force changes in others

Violates single responsibility principle

Here's a typical example of Circular Dependencies:

// UserService directly depends on OrderService
public class UserService {
    private OrderService orderService;
    
    public UserService(OrderService orderService) {
        this.orderService = orderService;
    }
    
    public User createUser(String name) {
        User user = new User(name);
        orderService.initializeUserOrders(user);
        return user;
    }
    
    public List<Order> getUserOrders(User user) {
        return orderService.getOrdersByUser(user);
    }
    
    public void updateUserOrderCount(User user) {
        user.setOrderCount(user.getOrderCount() + 1);
    }
}

// OrderService directly depends on UserService  
public class OrderService {
    private UserService userService;
    
    public OrderService(UserService userService) {
        this.userService = userService; // Circular dependency!
    }
    
    public void initializeUserOrders(User user) {
        Order defaultOrder = new Order(user.getId(), "Welcome Order");
        userService.updateUserOrderCount(user);
    }
    
    public List<Order> getOrdersByUser(User user) {
        return new ArrayList<>();
    }
}

public class User {
    private String id;
    private String name;
    private int orderCount;
    
    public User(String name) {
        this.id = UUID.randomUUID().toString();
        this.name = name;
        this.orderCount = 0;
    }
    
    public String getId() { return id; }
    public String getName() { return name; }
    public int getOrderCount() { return orderCount; }
    public void setOrderCount(int orderCount) { this.orderCount = orderCount; }
}

public class Order {
    private String id;
    private String userId;
    private String description;
    
    public Order(String userId, String description) {
        this.id = UUID.randomUUID().toString();
        this.userId = userId;
        this.description = description;
    }
    
    public String getId() { return id; }
    public String getUserId() { return userId; }
    public String getDescription() { return description; }
}

In this example, UserService and OrderService directly depend on each other through their constructors, creating a circular dependency. This makes it impossible to instantiate either service (since each requires the other to be created first) and creates tight coupling that violates clean code principles.

The Refactoring: Dependency Inversion

The Dependency Inversion refactoring breaks circular dependencies by introducing abstractions (interfaces) and ensuring that high level modules depend on abstractions rather than concrete implementations.

Step by step refactoring process:

  1. Identify the circular dependency between classes.

  2. Extract a focused interface that represents only the specific functionality needed by the dependent class.

  3. Make one class implement the interface and the other depend on the interface instead of the concrete class.

  4. Ensure single responsibility and loose coupling for each class.

Here's the refactored code:

// Step 1: Extract interface to break circular dependency
public interface UserOrderNotifier {
    void updateUserOrderCount(User user);
}

// Step 2: Refactor UserService to implement the interface
public class UserService implements UserOrderNotifier {
    private OrderService orderService;
    
    public UserService() {
        this.orderService = new OrderService(this);
    }
    
    public User createUser(String name) {
        User user = new User(name);
        orderService.initializeUserOrders(user);
        return user;
    }
    
    public List<Order> getUserOrders(User user) {
        return orderService.getOrdersByUser(user);
    }
    
    @Override
    public void updateUserOrderCount(User user) {
        user.setOrderCount(user.getOrderCount() + 1);
    }
}

// Step 3: Refactor OrderService to depend on interface instead of concrete class
public class OrderService {
    private final UserOrderNotifier userOrderNotifier;
    
    public OrderService(UserOrderNotifier userOrderNotifier) {
        this.userOrderNotifier = userOrderNotifier;
    }
    
    public void initializeUserOrders(User user) {
        Order defaultOrder = new Order(user.getId(), "Welcome Order");
        userOrderNotifier.updateUserOrderCount(user);
    }
    
    public List<Order> getOrdersByUser(User user) {
        return new ArrayList<>();
    }
}

// Step 4: Usage example showing how the refactored code works
public class Main {
    public static void main(String[] args) {
        // Create UserService (which implements UserOrderNotifier)
        UserService userService = new UserService();
        
        // Create a user - this will automatically initialize orders
        User user = userService.createUser("John Doe");
        
        // Get user orders
        List<Order> orders = userService.getUserOrders(user);
        
        System.out.println("User: " + user.getName());
        System.out.println("Order Count: " + user.getOrderCount());
        System.out.println("Orders: " + orders.size());
    }
}
Output:
User: John Doe
Order Count: 1
Orders: 0

Benefits of Dependency Inversion Refactoring

Benefit

Description

Eliminates Circular Dependencies

Breaks the tight coupling between classes by introducing interfaces as abstractions

Improves Testability

Classes can be tested independently by mocking their dependencies through interfaces

Enhances Maintainability

Changes to one class don't require changes to dependent classes, following the Open/Closed Principle

Promotes Loose Coupling

Classes depend on abstractions rather than concrete implementations, making the system more flexible

Facilitates Dependency Injection

Dependencies can be injected at runtime, making the system more configurable and testable

When to Apply Dependency Inversion Refactoring

  • When you encounter compilation errors due to circular imports between classes.

  • When classes are tightly coupled and changes in one force changes in others.

  • When unit testing becomes difficult because of interdependent class relationships.

  • When you need to implement the Dependency Inversion Principle from SOLID.

  • When you want to make your code more modular and maintainable.

  • When you're implementing dependency injection patterns.

Repository & Resources

Complete Code Examples: Clean Code Repository

Find the complete implementation of Circular Dependencies refactoring and other clean code techniques in our dedicated repository. Each example includes:

  • Before and after code comparisons

  • Unit tests demonstrating the improvements

Found this helpful? Share it with a colleague who's struggling with Circular Dependencies. Have questions about refactoring Dependency Inversion in your codebase? Email us directly, we read every message and the best questions become future newsletter topics.