Development

7 min read

How to Integrate External Data into Broadleaf Products using Custom Providers

Nathan Moore

Written by Nathan Moore

Published on Oct 28, 2025

When building complex eCommerce applications with Broadleaf Commerce, you'll often need to integrate external services to enrich your product data. In this post, we'll explore how to customize Broadleaf's provider components to fetch and integrate external service data into your product details pages.

Understanding Orchestration and Providers

Before diving into customization, let's review two key Broadleaf concepts:

Orchestration is when a microservice makes requests to other microservices to gather and manage data. This is particularly useful when there's complex logic required to aggregate data from multiple sources. Instead of API callers performing aggregation themselves, they can make a single request to an orchestration service like catalog browse.

Providers are service layer components that abstract the process of performing API requests. They handle:

  • Taking input data and building web requests
  • Executing requests against known API URLs
  • Deserializing responses back into Java objects
  • Returning processed data to the caller

This abstraction allows other service layer components to get the data they need through simple Java method calls, without knowing the specifics of API request processing.

The Challenge: Integrating External Label Data

Let's consider a practical scenario: you have an external service that contains label information about products, and you want this data to appear on your product details pages. The Catalog Browse service needs to fetch this external data and include it in its responses.

The Catalog Browse service exposes two primary entry points for retrieving product information, and both are relevant for a Product Details Page (PDP):

  • /browse/details: This endpoint is typically used for URL resolution. It consumes a URL slug (e.g., /electronics/laptop-x) and resolves it to the appropriate entity (Product, Category, or Content). When it resolves to a Product, it returns the full product details.
  • /browse/products: This is used for direct lookups when the client already knows the Product ID and is specifically fetching the product details. This is common for operations that require a direct lookup.

Crucially, the product data returned by both endpoints is functionally the same for a PDP. Therefore, to guarantee that our custom external label data is present and consistent, regardless of which entry point is used to fetch the product, we must customize the provider logic underlying both operations: fetchBrowseEntityDetails and fetchProductsWithDetails.

Implementation Strategy

Our approach involves extending the existing ExternalCatalogProvider (Broadleaf's foundational component designed for integrating external catalog data from PIMs or other systems) and overriding two key methods:

  • fetchBrowseEntityDetails
  • fetchProductsWithDetails

We'll also create a new provider component called CustomExternalServiceProvider to handle the actual API requests to our external service.

Code Implementation

Step 1: Create the External Catalog Provider

import org.springframework.web.reactive.function.client.WebClient;

import com.broadleafcommerce.catalogbrowse.domain.BrowseDetailsRequest;

import com.broadleafcommerce.catalogbrowse.domain.BrowseEntityDetails;

import com.broadleafcommerce.catalogbrowse.domain.Product;

import com.broadleafcommerce.catalogbrowse.domain.ProductDetailsRequest;

import com.broadleafcommerce.catalogbrowse.domain.ProductList;

import com.broadleafcommerce.catalogbrowse.service.provider.PricingProvider;

import com.broadleafcommerce.catalogbrowse.service.provider.external.catalog.ExternalCatalogProvider;

import com.broadleafcommerce.common.extension.TypeFactory;

import com.fasterxml.jackson.databind.ObjectMapper;

import com.jostens.ecom.catalogbrowse.domain.ExternalServiceData;

import com.jostens.ecom.catalogbrowse.provider.AcmeCustomExternalServiceProvider;

import java.util.List;

import java.util.Set;

import java.util.stream.Collectors;

public class AcmeExternalCatalogProvider extends ExternalCatalogProvider {

    private final AcmeCustomExternalServiceProvider externalServiceProvider;

    /**

     * Constructor must pass the low-level, infrastructural dependencies (WebClient, ObjectMapper, etc.) 

     * required by the base class, plus our custom service.

     */

    public AcmeExternalCatalogProvider(

        WebClient catalogBrowseWebClient,

        ObjectMapper mapper,

        PricingProvider browsePricingProvider,

        TypeFactory typeFactory,

        AcmeCustomExternalServiceProvider externalServiceProvider) {

        // Pass the required components to the base class constructor

        super(catalogBrowseWebClient, mapper, browsePricingProvider, typeFactory);

        this.externalServiceProvider = externalServiceProvider;

    }

    /**

     * Overrides the method corresponding to the /browse/details endpoint (URL resolution).

     */

    @Override

    public BrowseEntityDetails fetchBrowseEntityDetails(BrowseDetailsRequest request) {

        BrowseEntityDetails result = super.fetchBrowseEntityDetails(request);

        // Enrich the data if a product entity was successfully resolved

        if (result != null && result.getProducts() != null) {

            fetchAndApplyExternalServiceData(result.getProducts());

        }

        return result;

    }

    /**

     * Overrides the method corresponding to the /browse/products endpoint (direct ID lookup).

     */

    @Override

    public ProductList fetchProductsWithDetails(ProductDetailsRequest request) {

        ProductList result = super.fetchProductsWithDetails(request);

        // Enrich the data

        if (result != null && result.getProducts() != null) {

            fetchAndApplyExternalServiceData(result.getProducts());

        }

        return result;

    }

    /**

     * Helper method to fetch and apply external data consistently across both entry points.

     */

    private void fetchAndApplyExternalServiceData(List<Product> products) {

        // Gather all unique product IDs

        Set<String> productIds = products.stream()

            .map(Product::getId)

            .collect(Collectors.toSet());

        // Fetch external labels using our isolated service component

        ExternalServiceData externalData = 

            externalServiceProvider.fetchExternalServiceData(productIds);

        // Apply the labels to the product attributes

        products.forEach(product -> {

            String label = externalData.getLabels().get(product.getId());

            if (label != null) {

                // Add the external data to the standard product attributes map

                product.getAttributes().put("externalLabel", label);

            }

        });

    }

}

Step 2: Create the External Service Data DTO

import java.util.Map;

import lombok.Data;

@Data // Provides constructors, getters, and setters

public class ExternalServiceData {

    private Map<String, String> labels; // Product ID -> Label mapping

}

Step 3: Implement the External Service Provider

@Component

@RequiredArgsConstructor

public class AcmeCustomExternalServiceProvider {

    // inject the CatalogBrowseClient as each service has its own

    @Qualifier("catalogBrowseWebClient")

    private final WebClient webClient;

    public ExternalServiceData fetchExternalServiceData(Set<String> productIds) {

        // For demo purposes, using mock data

        return createMockData(productIds);

    }

    // Real implementation would look like this:

    private ExternalServiceData fetchFromRealService(Set<String> productIds) {

        try {

            Map<String, String> response = webClient

                .get()

                .uri(uriBuilder -> uriBuilder

                    .path("/external-labels")

                    .queryParam("productIds", String.join(",", productIds))

                    .build())

                .attributes(clientRegistrationId("external-service")) // For OAuth

                .retrieve()

                .onStatus(HttpStatusCode::isError, clientResponse -> {

                    // Handle errors with custom exception

                    return Mono.error(new ExternalServiceException("Failed to fetch labels"));

                })

                .bodyToMono(new ParameterizedTypeReference<Map<String, String>>() {})

                .block();

            ExternalServiceData data = new ExternalServiceData();

            data.setLabels(response);

            return data;

        } catch (Exception e) {

            // Handle exceptions appropriately

            throw new ExternalServiceException("Error calling external service", e);

        }

    }

    private ExternalServiceData createMockData(Set<String> productIds) {

        Map<String, String> mockLabels = productIds.stream()

            .collect(Collectors.toMap(

                id -> id,

                id -> "Mock Label for " + id

            ));

        ExternalServiceData data = new ExternalServiceData();

        data.setLabels(mockLabels);

        return data;

    }

}

Key Implementation Details

Using Spring WebClient

The framework uses Spring's WebClient for making HTTP requests from providers. This provides rich features for:

  • Building and sending requests
  • Processing responses
  • Handling authentication (OAuth via client registration)
  • Error handling and retries

Security Considerations

If your external API requires authentication, you can use the clientRegistrationId attribute to configure OAuth token acquisition. This follows standard Spring Security OAuth configuration patterns.

Error Handling

Always implement proper error handling for external service calls:

  • Wrap API errors in custom exceptions
  • Provide fallback behavior when external services are unavailable
  • Log appropriate details for debugging

Response Processing

Use Jackson for automatic deserialization of JSON responses to Java objects. This eliminates manual JSON parsing and reduces error-prone code.

Testing the Integration

Once implemented, you can verify the integration by:

  1. Making a request to your product details page
  2. Inspecting the network response payload
  3. Looking for the external_label field in the product attributes

The external label data will now be available for frontend display purposes, seamlessly integrated into your existing product data structure.

Conclusion

Customizing Broadleaf providers allows you to seamlessly integrate external services into your eCommerce platform. By extending existing providers and implementing clean abstractions for external API calls, you can enrich your product data without disrupting existing functionality.

The key benefits of this approach include:

  • Separation of concerns - External service logic is isolated in dedicated components
  • Consistency - The same external data appears across all relevant endpoints
  • Maintainability - Changes to external service integration don't affect core business logic
  • Testability - External service calls can be easily mocked for testing

This pattern can be applied to integrate any external service, whether for product enrichment, inventory checks, pricing data, or other business-critical information.