Dependency Pull and Contextualized Dependency Lookup in Spring Framework

Dependency Pull and Contextualized Dependency Lookup in Spring Framework

Dependency Pull and Contextualized Dependency Lookup IoC Types in Spring Framework

In this tutorial, we are going to look into the core concept of IoC, i.e dependency pull and dependency lookup in the Spring Framework.

Dependency Pull

In the dependency pull mechanism, our component explicitly reaches out to the external container, registry, to pull the dependencies whenever needed, instead of injecting dependencies into our components at startup.
Although the control is still inverted i.e the control of object creation and lifecycle management is transferred to the framework.
java
public class MainApplication {
    public static void main(String[] args) {
        ApplicationContext container = new AnnotationConfigApplicationContext(NotificationAppConfig.class);

        NotificationService manager = container.getBean(NotificationService.class);
       
        manager.sendAlert("Your package has shipped!", "john.doe@example.com");
    }
}
Let's look into these code snippets. The NotificationService bean is retrieved from the ApplicationContext, which has all the registered beans in the application scope.
This instance is pulled so that its method sendAlert can be invoked to function.

When is Dependency Pull actually useful?

The dependency pull is used while developing plugins, uses dynamic environments where the implementation doesn't know which beans are required and must be fetched conditionally based on runtime data.
Let's look at the example of building a multitenant payment processor system where each tenant can choose their own payment processor. Here, we can't implement the single payment processor at startup need to provide the payment processor according to the tenant.
Here is a sample example to demonstrate this.
java
package org.csbyte.payment;

import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/payments")
public class PaymentController {

    private final ApplicationContext context;

    public PaymentController(ApplicationContext context) {
        this.context = context;
    }

    @PostMapping("/process")
    public void processTenantPayment(
        @RequestHeader("X-Tenant-ID") String tenantId, 
        @RequestBody PaymentPayload payload
    ) {
        // Look up the tenant's configuration from a database
        String processorBeanName = tenantRegistry.getProcessorFor(tenantId); // e.g., "stripeProcessor"
        
        // PULL: Dynamically fetch the correct tenant-specific bean from the context at runtime
        PaymentProcessor processor = context.getBean(processorBeanName, PaymentProcessor.class);
        
        processor.charge(payload.amount());
    }
}

Contextualized Dependency Lookup

Dependency lookup is a similar term in some aspect to the dependency pull, but in CDL, the lookup is from the container that manages the resources, not from the central registry.
Let's look at a real-world example to understand CDL. It is like a guest chef working in different kitchens. Instead of bringing their own tools(Dependency Injection) or searching the global market(Dependency pull), the chef relies on the kitchen manager to hand them specific tools to work on.
java
package org.csbyte.cdl;

// The CDL Lifecycle Contract
interface RegionManagedComponent {
    void initializeContext(PaymentContainer container);
}

// The Context Container Interface
interface PaymentContainer {
    Object getSecureResource(String resourceName);
}

// Concrete Container managing localized secure infrastructure
class USARegionContainer implements PaymentContainer {
    @Override
    public Object getSecureResource(String resourceName) {
        if ("encryptionKey".equals(resourceName)) {
            return "USA-AES-KEY-99X72";
        }
        throw new IllegalArgumentException("Resource not found in USA Region: " + resourceName);
    }
}

// The Provider Dependency
interface CryptoEngine {
    String encryptData(String rawData);
}

class OpenSSLCryptoEngine implements CryptoEngine {
    private final String key;

    public OpenSSLCryptoEngine(String key) {
        this.key = key;
    }

    @Override
    public String encryptData(String rawData) {
        return "[Encrypted with " + key + "]: " + rawData;
    }
}

// The Renderer equivalent: The component executing business logic
interface PaymentProcessor extends RegionManagedComponent {
    void processTransaction(double amount);
}

class CreditCardProcessor implements PaymentProcessor {
    private CryptoEngine cryptoEngine;

    // CDL Implementation: The component waits to be handed the container context
    @Override
    public void initializeContext(PaymentContainer container) {
        // Look up the exact localized resource using the container provided at initialization
        String regionalKey = (String) container.getSecureResource("encryptionKey");
        this.cryptoEngine = new OpenSSLCryptoEngine(regionalKey);
    }

    @Override
    public void processTransaction(double amount) {
        if (cryptoEngine == null) {
            throw new IllegalStateException("Processor uninitialized! Context must be loaded first.");
        }
        String securePayload = cryptoEngine.encryptData("Card: 4111xxxx; Amount: $" + amount);
        System.out.println("Dispatching transaction payload: " + securePayload);
    }
}

public class BillingSystemDemo {
    public static void main(String... args) {
        // Initialize the local container infrastructure
        PaymentContainer usaContainer = new USARegionContainer();

        // Instantiate the component (currently completely detached and empty)
        PaymentProcessor processor = new CreditCardProcessor();

        // Contextualization: Hand the local region infrastructure container to the component
        processor.initializeContext(usaContainer);

        // Execute business method safely
        processor.processTransaction(250.75);
    }
}
Instantiation:
java
PaymentProcessor processor = new CreditCardProcessor();
When CreditCardProcessor is created, its internal cryptoEngine field is null
Contextualization (The Inversion of Control):
java
processor.initializeContext(usaContainer);
Instead of the CreditCardProcessor using a global system setting, it actively hands the localized context container (usaContainer) to the component.
  • The Component Shows Up Empty: The component (our chef) is created with absolutely nothing inside it. It doesn’t have its tools, and it doesn't try to look for them yet.
  • The Container Hands Over the Context: The framework (the kitchen manager) actively approaches the component and hands it a specific container context (the workspace box).
  • The Component Looks Up What it Needs: Now that the component is holding the box, it reaches inside and looks up the exact tools it needs for that specific environment.

When is Contextualized Dependency Lookup actually useful?

It is useful when the same component needs to behave differently depending on where or when it is running
If you hand the component for the US region, it looks inside, pulls out a US encryption key, and processes American dollars. If you hand that exact same component for the Europe region, it looks inside, pulls out a European encryption key, and processes Euros.