Development
7 min readWhen 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.
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:
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.
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):
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.
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:
We'll also create a new provider component called CustomExternalServiceProvider to handle the actual API requests to our external service.
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);
}
});
}
}
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
}
@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;
}
}
The framework uses Spring's WebClient for making HTTP requests from providers. This provides rich features for:
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.
Always implement proper error handling for external service calls:
Use Jackson for automatic deserialization of JSON responses to Java objects. This eliminates manual JSON parsing and reduces error-prone code.
Once implemented, you can verify the integration by:
The external label data will now be available for frontend display purposes, seamlessly integrated into your existing product data structure.
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:
This pattern can be applied to integrate any external service, whether for product enrichment, inventory checks, pricing data, or other business-critical information.