Skip to main content

Design a Package Manager/Installer with Dependency Resolution

Problem Statement

Design a Package Manager and Installer engine (similar to npm, apt-get, or Maven dependency managers). The engine must allow registering packages along with their corresponding lists of dependencies, resolve the correct order of installation dynamically using topological sorting so that all dependencies are installed before their parent packages, and detect circular dependencies (e.g., Package X depends on Package Y, and Y depends on X), throwing an exception to abort execution.

Asked In Companies
Amazon Salesforce RedHat

Design Decisions & Patterns Used

In modern software development, applications depend on reusable library packages. These libraries in turn depend on other libraries, forming a complex dependency graph. When a user requests to install a package, the system must traverse this graph, identify all nested dependencies, and order them so that libraries are installed in a bottom-up sequence.

To accomplish this, we model the package relationships as a **Directed Graph** where packages are nodes and dependencies are directed edges. Resolving the installation order is equivalent to executing a **Topological Sort** on this graph. If there are circular references in the graph (cycles), the topological sort is impossible, and the program must fail fast to prevent deadlocks or infinite download loops.

We will utilize the following Design Patterns:

  • Strategy Pattern: We define a generic interface for dependency resolution. This allows us to swap the resolution algorithm at runtime (e.g., swapping a standard DFS-based topological sort with Kahn's BFS-based algorithm) depending on performance constraints.
  • Builder Pattern: Providing a fluent API to define packages and wire their dependencies programmatically, improving the readability of client code.

Functional Requirements

  • Maintain a global registry repository where packages can be registered with their name, version, and dependencies.
  • Resolve the transient dependency graph of any target package to build the correct order of installation.
  • Detect and reject circular dependency loops (e.g., Package A depends on B, B depends on C, and C depends on A).
  • Simulate downloading and installing the packages in the resolved order.

Objects Required

  • Package (The domain entity storing the package metadata, version, and a list of dependency names)
  • DependencyResolver (The strategy interface defining the contract for topological sorting algorithms)
  • DfsResolver (Concrete strategy implementation resolving packages using Depth-First Search)
  • PackageManager (The main coordinator class managing the package registry and executing installations)

Package Class

The Package class encapsulates the metadata of a single software library, including its name, version, and a list of dependency names.


import java.util.ArrayList;
import java.util.List;

public class Package {
    private final String name;
    private final String version;
    private final List<String> dependencies;

    public Package(String name, String version) {
        this.name = name;
        this.version = version;
        this.dependencies = new ArrayList<>();
    }

    public void addDependency(String packageName) {
        dependencies.add(packageName);
    }

    public String getName() { return name; }
    public String getVersion() { return version; }
    public List<String> getDependencies() { return dependencies; }
}

Let's break down the logic of this class:

  • Package(name, version): The constructor initializes the package with its name and version, and instantiates an empty list to store dependencies.
  • addDependency(packageName): Adds a required dependency package name to our list of dependencies. This is used by the package registry to build the edges of the dependency graph.

DependencyResolver Interface & DfsResolver Class

The DependencyResolver interface defines the contract for resolving dependencies. We apply the **Strategy Pattern** to keep the topological sorting algorithm pluggable, allowing us to implement different sorting strategies (like DFS or BFS).


import java.util.List;
import java.util.Map;

public interface DependencyResolver {
    List<Package> resolve(String targetPackage, Map<String, Package> registry);
}

Let's implement the concrete DFS-based topological sort strategy:


import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class DfsResolver implements DependencyResolver {
    @Override
    public List<Package> resolve(String targetPackage, Map<String, Package> registry) {
        List<Package> resolvedOrder = new ArrayList<>();
        Set<String> visiting = new HashSet<>();
        Set<String> visited = new HashSet<>();

        resolveDfs(targetPackage, registry, resolvedOrder, visiting, visited);
        return resolvedOrder;
    }

    private void resolveDfs(String pkgName, Map<String, Package> registry, 
                            List<Package> resolvedOrder, Set<String> visiting, Set<String> visited) {
        
        // Circular dependency detection
        if (visiting.contains(pkgName)) {
            throw new IllegalStateException("Circular Dependency Error: Loop detected involving package '" + pkgName + "'");
        }

        if (!visited.contains(pkgName)) {
            Package pkg = registry.get(pkgName);
            if (pkg == null) {
                throw new IllegalArgumentException("Dependency Resolution Error: Missing package '" + pkgName + "' in registry.");
            }

            visiting.add(pkgName);

            // Recursively resolve all dependencies first
            for (String depName : pkg.getDependencies()) {
                resolveDfs(depName, registry, resolvedOrder, visiting, visited);
            }

            visiting.remove(pkgName);
            visited.add(pkgName);
            resolvedOrder.add(pkg); // Add parent package once all dependencies are resolved
        }
    }
}

Let's break down the logic of every method in the DfsResolver class:

  • resolve(targetPackage, registry): This method initializes the helper collections. The visiting set tracks nodes currently in the active recursion path (to detect cycles), while the visited set tracks nodes that have been fully resolved (to prevent duplicate processing). It then calls the recursive helper method to compile the installation order.
  • resolveDfs(pkgName, registry, ...): This recursive helper implements our depth-first search. First, it checks if the package is in the visiting set. If it is, a circular dependency has been detected (the graph contains a cycle), so it throws an IllegalStateException. If the node has not yet been fully processed, it checks if it exists in the registry, adds it to the visiting set, recursively resolves all its dependencies, removes it from the visiting set, adds it to the visited set, and appends the package to the installation list.

PackageManager Class

The PackageManager class coordinates the overall workflow. It registers packages, initiates the dependency resolver strategy, and prints the simulated installation steps.


import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class PackageManager {
    private final Map<String, Package> registry;
    private final DependencyResolver resolver;

    public PackageManager(DependencyResolver resolver) {
        this.registry = new HashMap<>();
        this.resolver = resolver;
    }

    public void registerPackage(Package pkg) {
        registry.put(pkg.getName(), pkg);
        System.out.println("Registered Package: " + pkg.getName() + " (v" + pkg.getVersion() + ")");
    }

    public void install(String packageName) {
        System.out.println("Initiating installation for package: " + packageName);
        
        try {
            List<Package> installOrder = resolver.resolve(packageName, registry);
            System.out.println("\n--- Resolved Installation Order ---");
            
            for (int i = 0; i < installOrder.size(); i++) {
                Package pkg = installOrder.get(i);
                System.out.println((i + 1) + ". Installing " + pkg.getName() + " (v" + pkg.getVersion() + ")... DONE");
            }
            System.out.println("\nSuccess: Package '" + packageName + "' and all dependencies installed successfully.");
        } catch (Exception e) {
            System.err.println("\nInstallation Failed: " + e.getMessage());
            throw e;
        }
    }
}

Let's break down the logic of every method in the PackageManager class:

  • PackageManager(resolver): The constructor registers the chosen dependency resolution strategy and initializes the package registry map using a standard HashMap.
  • registerPackage(pkg): Registers a package in the manager's memory registry, mapping the package name to its configuration object.
  • install(packageName): Initiates the installation process. It calls the resolver strategy to calculate the installation sequence, logs the resolved order, and prints simulated download progress. If any error occurs, it catches the exception and logs the failure trace.

Main Driver Class

This class tests our package manager, verifying dependency resolution, installation ordering, and circular dependency checks.


public class Main {
    public static void main(String[] args) {
        // Instantiate package manager with a DFS dependency resolver
        PackageManager pm = new PackageManager(new DfsResolver());

        System.out.println("==========================================");
        System.out.println("Scenario 1: Testing Successful Installation");
        System.out.println("==========================================");

        // Define packages:
        // PackageA depends on PackageB and PackageC
        // PackageB depends on PackageD
        // PackageC has no dependencies
        // PackageD has no dependencies
        
        Package pkgA = new Package("PackageA", "1.0.0");
        pkgA.addDependency("PackageB");
        pkgA.addDependency("PackageC");

        Package pkgB = new Package("PackageB", "2.1.0");
        pkgB.addDependency("PackageD");

        Package pkgC = new Package("PackageC", "1.2.0");
        Package pkgD = new Package("PackageD", "0.9.0");

        pm.registerPackage(pkgA);
        pm.registerPackage(pkgB);
        pm.registerPackage(pkgC);
        pm.registerPackage(pkgD);

        // Install PackageA (Expected order: D, B, C, A)
        pm.install("PackageA");

        System.out.println("\n==========================================");
        System.out.println("Scenario 2: Testing Circular Dependency Loop");
        System.out.println("==========================================");

        PackageManager pmInvalid = new PackageManager(new DfsResolver());

        // Cyclic Loop: X depends on Y, Y depends on Z, Z depends on X
        Package pkgX = new Package("PackageX", "1.0.0");
        pkgX.addDependency("PackageY");

        Package pkgY = new Package("PackageY", "1.0.0");
        pkgY.addDependency("PackageZ");

        Package pkgZ = new Package("PackageZ", "1.0.0");
        pkgZ.addDependency("PackageX");

        pmInvalid.registerPackage(pkgX);
        pmInvalid.registerPackage(pkgY);
        pmInvalid.registerPackage(pkgZ);

        try {
            pmInvalid.install("PackageX");
        } catch (Exception e) {
            System.out.println("Caught Expected Exception: " + e.getMessage());
        }
    }
}

The main() driver configures packages and dependencies, verifies that the manager resolves the installation order (installing dependencies first), and asserts that cyclic dependencies are detected and rejected.


Also See

Comments

Popular posts from this blog

Designing a Parking Lot - Low Level Design

Problem Statement Design a parking lot that can handle vehicles entering and leaving while managing parking across multiple floors. Each vehicle should be assigned a suitable parking spot based on its type, and the spot should be freed once the vehicle exits. The design should also support generating a ticket at entry and optionally calculating the parking fee based on the duration of stay. Asked In Companies Amazon Google Microsoft Uber Walmart Flipkart Meta PayPal Oracle Salesforce Adobe Apple Intuit LinkedIn Atlassian Functional Requirements The design should support multiple vehicle types such as bikes, cars, and trucks A vehicle must be assigned a parking spot compatible with its type A parking spot cannot be assigned to more than one vehicle at a time The parking lot should support multiple levels (floors) The design should search and allocate an availa...

Most Frequently Asked Low Level Design(LLD) Interview Questions

Below are the curated list of most commonly asked Low Level Design (LLD) interview problems. Each problem includes a short description and a link to the complete solution with code and class diagrams. Design Parking Lot System The system should handle parking for different vehicle types such as bikes, cars, and trucks. It should manage slot allocation, availability tracking, and entry/exit flow. The design also ensures efficient usage of parking space under varying load conditions. View Solution Design Elevator / Lift System The system should support multiple elevators operating across floors with request handling logic. It focuses on scheduling algorithms to minimize wait time and optimize movement. It also manages direction control and concurrent floor requests. View Solution Design Movie Ticket Booking System The system should allow users to browse movies, select shows, and book seats. It handles seat ...

Software Design Patterns for LLD Interviews: A Complete Guide

Software Design Patterns for LLD Interviews: A Complete Guide In Software Development Engineer (SDE) interviews—especially for mid-level and senior roles—low-level design (LLD) rounds assess your ability to write clean, reusable, maintainable, and extensible code. The foundation of resolving these architectural challenges lies in the standard Gang of Four (GoF) Design Patterns. Rather than memorizing theoretical definitions, interviewers expect you to apply these patterns to real-world scenarios, identifying the trade-offs of each. Below is a comprehensive guide to the 12 most frequently asked design patterns in LLD interviews, categorized by their classification (Creational, Structural, and Behavioral). Each pattern contains a concrete, real-world Java implementation and a detailed breakdown of design decisions. Creational Design Patterns Creational design patterns deal with object creation mechanisms. They abstract the instantiation process, making a system independent of how...