Development

5 min read

Interlink Client: How Broadleaf Calls Co-Located Services Without Going Over the Wire

Nathan Moore

Written by Nathan Moore

Published on May 26, 2026

interlink client

In the world of modern commerce architecture, the debate between monoliths and microservices often boils down to a single trade-off between developer velocity and operational complexity. Microservices offer genuine modularity and scalability but bring a "distributed tax" from network latency, serialization overhead, and propagating security and request context across service boundaries. 

Broadleaf's Interlink Client creates an exception to that tax.

When a target service lives in the same JVM as the caller, the Client bypasses the network and invokes the controller method directly: No serialization, no latency, no extra context management. But when the target doesn't, the call goes out over HTTPS without intervention. The caller doesn’t need to know which JVM they or the target is in—the Client already does.

The Problem: The "Distributed Tax"

In a traditional microservices environment, every inter-service call looks like this:

  1. Serialize the request to JSON.
  2. Open a network connection (HTTPS/TLS).
  3. Transmit data over the wire.
  4. Authorize the request at the target service.
  5. Deserialize the request.
  6. Execute the logic.

A checkout flow that fans out to Catalog, Offer, Cart, and Fulfillment can pay this overhead four times per request. In a FlexPackage deployment, where those services share a JVM, none of that overhead buys you isolation.

The Solution: Loopback Optimization

The Broadleaf Interlink Client uses Spring WebFlux's WebClient. For remote services it behaves like any other WebClient, using REST over HTTP. However, for services it can resolve in the local JVM, it skips the HTTP path entirely, using what we call Loopback Optimization.

How it Works: Direct Reflective Calls

At startup, Interlink inspects the application context for InterlinkReflectionInfo beans matching the requested logical service. If it finds one, it dispatches the call reflectively against the in-JVM controller. This means:

  • Zero Network Latency: No packets leave the JVM.
  • No Serialization Overhead: Arguments are passed as Java objects where possible.
  • No Security Handshake: Since the call is internal, the costly TLS handshake is skipped.

If the service is not local, Interlink falls back to a standard WebClient call over HTTPS. The calling code remains identical.

Seamless Developer Experience

Tenant IDs, sandbox IDs, and localization settings ride along with every call. Interlink handles this for you.

  • For Remote Calls: It serializes the current ContextInfo into an X-CONTEXT-REQUEST header.
  • For Local Calls: It reconstructs the ContextInfo object and injects it directly into the reflective method call using a mock web request.

Unified Request Model

Interlink uses a single InterlinkRequest type for every call, so you don't need a generated client interface per endpoint. This model abstracts away the differences between a network call and a local method invocation.

InterlinkRequest request = new InterlinkRequest("readProduct")

.withRequestMethod(HttpMethod.GET)

.withUri("https://catalog/products/{id}")

.withArguments(List.of(

new UrlArgument("id", "123"), // For the remote URI template

new MethodArgument(0, "123") // For the local method's first parameter

));

// Interlink decides: Is catalog-service local? 

// If yes -> Reflection call. If no -> WebClient call.

String responseJson = interlinkClient.invoke(request);

Configuring Loopback

To enable this optimization, you simply provide InterlinkReflectionInfo beans. These beans tell the client which controller methods map to which logical service names.

@Bean

public InterlinkReflectionInfo productReflection() {

return new InterlinkReflectionInfo(

"readProduct", 

"com.broadleafcommerce.catalog.web.endpoint.ProductEndpoint", 

"readProductById", 

List.of("java.lang.String", "com.broadleafcommerce.data.tracking.core.context.ContextInfo")

);

}

Best Practices & Testing

While reflection is powerful, it can be fragile. If a developer changes a controller method signature without updating the corresponding InterlinkReflectionInfo, the loopback will fail at runtime.

To mitigate this, Broadleaf recommends three key strategies:

  1. Validation Tests: Implement build-time unit tests (e.g., InterlinkReflectionValidationTest) that instantiate your configuration and verify that the InterlinkReflectionInfo bean correctly resolves to the intended method. This catches mismatches at build time rather than runtime.
  2. Explicit Documentation: When writing a controller method targeted by Interlink, add a comment referencing the reflection bean. This serves as a warning to other developers that the method signature is part of an internal "contract."
  3. Constants for Names: Use a shared utility class (like InterlinkUtils) to store the string identifiers for your interlink requests to ensure consistency between the caller and the configuration.

A Phased Rollout

Broadleaf is rolling the Interlink pattern out across the framework. Not every WebClient has been converted yet, but you can see it in action today in places like the Search Service, where it cuts latency during product indexing, and in several large-scale client deployments where performance was a hard requirement.

Conclusion: The FlexPackage Edge

The Interlink Client is the backbone of Broadleaf's FlexPackage deployment strategy. It lets you develop and deploy in a "balanced" state, scaling out into full microservices when you need them and retaining the efficiency of a monolith when co-location makes more sense, no more distributed tax.

The result: the same codebase ships as a distributed system in production and as a single process in local development or smaller deployments, with no change to the calling code.
Full documentation lives on the Broadleaf Developer Portal.

Related Resources