Skip to main content

Design an Extensible Dependency Injection (DI) Container

Problem Statement

Design an extensible Dependency Injection (DI) Container (similar to Spring Core or Google Guice). The container must scan and register component definitions (Beans), manage bean lifecycles (Singleton vs. Prototype scopes), resolve dependency graphs recursively using reflection, and dynamically detect and reject circular dependencies (e.g., Service A depends on Service B, which depends on Service A).

Asked In Companies
Google Salesforce

Design Decisions & Patterns Used

Modern enterprise frameworks rely heavily on Inversion of Control (IoC). An IoC container manages the creation and lifecycle of objects, wire dependencies automatically, and inject them where needed. To achieve this, we use Java Reflection to inspect constructors, resolve parameters recursively, and detect circular dependencies using a stack-based tracker.

We will utilize the following Design Patterns:

  • Registry Pattern: Storing meta-definitions (BeanDefinition) mapping bean classes to their scopes and configuration options.
  • Factory Pattern: Decoupling bean creation from usage, instantiating classes dynamically at runtime.
  • Proxy Pattern: Injecting lazy-loaded wrapper classes (optional, but essential for dynamic scoping).

Functional Requirements

  • Register class mapping configurations dynamically.
  • Support two bean scopes: SINGLETON (a single shared instance is cached) and PROTOTYPE (a new instance is created on every request).
  • Inspect constructors using reflection to resolve and inject constructor dependencies recursively.
  • Track instantiation paths. Throw a descriptive exception if a circular dependency is detected.

Objects Required

  • Scope (Enum mapping SINGLETON and PROTOTYPE lifecycles)
  • BeanDefinition (Data container holding class metadata and configuration parameters)
  • DiContainer (Core IoC registry resolving dependencies and caching instances)

Scope Enum & BeanDefinition Class

The Scope enum defines the lifecycle rules of bean instances, and the BeanDefinition class encapsulates configuration details.


public enum Scope {
    SINGLETON,
    PROTOTYPE
}

Let's define the BeanDefinition class:


public class BeanDefinition {
    private final Class<?> beanClass;
    private final Scope scope;

    public BeanDefinition(Class<?> beanClass, Scope scope) {
        this.beanClass = beanClass;
        this.scope = scope;
    }

    public Class<?> getBeanClass() { return beanClass; }
    public Scope getScope() { return scope; }
}

The constructor registers the bean class and its lifecycle rules, which are evaluated by the container.


DiContainer Class

The DiContainer registers beans, resolves constructor parameters recursively using reflection, and maintains a recursion stack to detect circular dependencies.


import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class DiContainer {
    private final Map<Class<?>, BeanDefinition> beanDefinitions;
    private final Map<Class<?>, Object> singletonInstances;
    private final Set<Class<?>> creationStack;

    public DiContainer() {
        this.beanDefinitions = new ConcurrentHashMap<>();
        this.singletonInstances = new ConcurrentHashMap<>();
        this.creationStack = Collections.synchronizedSet(new HashSet<>());
    }

    public void registerBean(Class<?> clazz, Scope scope) {
        beanDefinitions.put(clazz, new BeanDefinition(clazz, scope));
        System.out.println("Registered Bean: " + clazz.getSimpleName() + " with scope: " + scope);
    }

    @SuppressWarnings("unchecked")
    public <T> T getBean(Class<T> clazz) {
        BeanDefinition definition = beanDefinitions.get(clazz);
        if (definition == null) {
            throw new RuntimeException("No bean definition found for class: " + clazz.getName());
        }

        // Return cached instance if it is a singleton and already instantiated
        if (definition.getScope() == Scope.SINGLETON && singletonInstances.containsKey(clazz)) {
            return (T) singletonInstances.get(clazz);
        }

        // Detect circular dependency using the creation stack
        if (creationStack.contains(clazz)) {
            throw new IllegalStateException("Circular dependency detected! Class " + clazz.getSimpleName() + 
                    " is already in the creation stack: " + creationStack);
        }

        creationStack.add(clazz);
        T instance = instantiateBean(clazz);
        creationStack.remove(clazz);

        if (definition.getScope() == Scope.SINGLETON) {
            singletonInstances.put(clazz, instance);
        }

        return instance;
    }

    @SuppressWarnings("unchecked")
    private <T> T instantiateBean(Class<T> clazz) {
        try {
            Constructor<?>[] constructors = clazz.getConstructors();
            if (constructors.length == 0) {
                // Try retrieving declared default constructor if no public constructors are found
                Constructor<T> defaultConstructor = clazz.getDeclaredConstructor();
                return defaultConstructor.newInstance();
            }

            // Standard rule: select the first public constructor
            Constructor<?> targetConstructor = constructors[0];
            Class<?>[] parameterTypes = targetConstructor.getParameterTypes();
            Object[] parameterInstances = new Object[parameterTypes.length];

            // Resolve constructor parameters recursively
            for (int i = 0; i < parameterTypes.length; i++) {
                parameterInstances[i] = getBean(parameterTypes[i]);
            }

            return (T) targetConstructor.newInstance(parameterInstances);
        } catch (Exception e) {
            throw new RuntimeException("Failed to instantiate bean: " + clazz.getName() + ". Error: " + e.getMessage(), e);
        }
    }
}

Here is an explanation of the core operations in the DiContainer class:

  • The constructor configures bean definition tables, singleton instance caches, and a thread-safe set tracking active bean instantiations.
  • registerBean() maps bean classes to their lifecycle scopes.
  • getBean() checks if the requested singleton is already cached. If not, it checks the creationStack to detect circular dependencies, adds the class to the stack, instantiates it, removes it from the stack, and caches singletons.
  • instantiateBean() uses Java Reflection to inspect constructors. It fetches constructor parameters, resolves them recursively using getBean(), and instantiates the class using the resolved parameter instances.

Main Driver Class

This class tests our DI container by simulating two scenarios: a successful dependency resolution (instantiating a Engine, injecting it into a Car, and resolving a Dealership), and a circular dependency failure.


// Mock classes for Scenario 1
class Engine {
    public void start() { System.out.println("Engine vroom... vroom!"); }
}

class Car {
    private final Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void drive() {
        engine.start();
        System.out.println("Car is moving.");
    }
}

// Mock classes for Scenario 2 (Circular Dependency)
class ServiceA {
    public ServiceA(ServiceB serviceB) {}
}

class ServiceB {
    public ServiceB(ServiceA serviceA) {}
}

public class Main {
    public static void main(String[] args) {
        DiContainer container = new DiContainer();

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

        container.registerBean(Engine.class, Scope.SINGLETON);
        container.registerBean(Car.class, Scope.PROTOTYPE);

        // Retrieve Car instance (should automatically instantiate Engine and inject it)
        Car myCar = container.getBean(Car.class);
        myCar.drive();

        // Verify that the Engine bean is managed as a Singleton (returns same instance)
        Engine e1 = container.getBean(Engine.class);
        Engine e2 = container.getBean(Engine.class);
        System.out.println("Engine instance match check: " + (e1 == e2) + " (Should be true)");

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

        DiContainer invalidContainer = new DiContainer();
        invalidContainer.registerBean(ServiceA.class, Scope.SINGLETON);
        invalidContainer.registerBean(ServiceB.class, Scope.SINGLETON);

        try {
            System.out.println("Retrieving ServiceA...");
            invalidContainer.getBean(ServiceA.class);
        } catch (Exception e) {
            System.out.println("Caught Expected Exception:\n" + e.getMessage());
        }
    }
}

The main() driver configures injection mappings, verifies recursive resolution of constructor dependencies, validates bean scopes, and asserts that circular dependency loops are correctly 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...