Client-Driven Filtering

👉 Implementing Field Selection via Query Parameters or Custom Headers in Spring Boot APIs.


What is Client-Driven filtering ?

Client-Driven Filtering is an API design pattern where the client dictates what data (fields) they want to receive in the response, typically via query parameters.

So, instead of the server always returning a full, potentially heavy representation of a resource, the client asks for a partial response.

This is crucial in microservices, especially when a single endpoint serves multiple clients with varying data needs, avoiding the classic Over-fetching problem.


Table of Contents


Purpose and Benefits

The primary objective of this approach is to improve network efficiency and application performance by minimizing the payload size and delegating the filtering responsibility to client.

AspectShort DescriptionDetailed Benefit
PurposeImprove Network Efficiency and Application PerformanceAchieved by minimizing the overall data payload size transferred between client and server.
Bandwidth ReductionDecreases data transfer volume.Especially beneficial for mobile clients or connections with high latency and limited bandwidth.
Faster Response TimeQuicker processing on both endsSmaller payloads reduce the time required for serialization/deserialization processes on the server and client.
Reduced Server LoadLower resource utilizationLess data to process in the application layer and potentially less strain on the database if filtering influences the data retrieval query.
API FlexibilitySingle, robust endpoint for diverse clientsAllows a single endpoint to efficiently serve various client needs (e.g., list views requiring minimal fields, detail views requiring all fields).

Implementation

Implementing client-driven filtering in a Spring Boot application involves two main types: Field Filtering (partial response) and Relationship Filtering (dynamic eager loading).

Field Filtering (Partial Response)

This involves selectively serializing fields in the DTO based on a client-provided list of desired fields (e.g., ?fields=id,name,price).

While custom logic is an option, using Spring Boot’s integration with Jackson library allows powerful dynamic filtering. Jackson’s @JsonFilter and FilterProvider can be used, although a simpler approach for partial response is often implemented using a custom MappingJacksonValue.

Controller

// ProductController.java

import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductController {

    @GetMapping("/products/{id}")
    public MappingJacksonValue getProductDetails(
            @PathVariable Long id, 
            @RequestParam(required = false) String fields) {
        
        Product product = productService.findById(id); // Assume this fetches the Product DTO

        // 1. Create a MappingJacksonValue for dynamic filtering
        MappingJacksonValue mapping = new MappingJacksonValue(product);

        if (fields != null && !fields.isEmpty()) {
            // 2. Define which properties to include
            String[] fieldArray = fields.split(",");
            SimpleBeanPropertyFilter filter = 
                SimpleBeanPropertyFilter.filterOutAllExcept(fieldArray);
            
            // 3. Register the filter with an arbitrary ID ("productFilter")
            FilterProvider filters = new SimpleFilterProvider()
                .addFilter("productFilter", filter);
            
            // 4. Apply the filter provider
            mapping.setFilters(filters);
        }
        
        return mapping;
    }
}

ProductDTO

// ProductDTO.java

import com.fasterxml.jackson.annotation.JsonFilter;

// The @JsonFilter annotation links the DTO to the filter ID ("productFilter")
@JsonFilter("productFilter") 
public class ProductDTO {
    private Long id;
    private String name;
    private double price;
    private String description;
    // Getters and Setters...
}

Relationship Filtering (Dynamic Eager Loading)

This allows the client to request related resources to be included (eagerly loaded) in the primary resource’s response (e.g., ?includes=category,reviews).

This is often implemented in the Service/Repository Layer by dynamically deciding which related entities to fetch from the database.

Implementation Strategy (JPA/Hibernate)

With JPA (e.g., Hibernate), this can be achieved by using Entity Graphs or dynamic JOIN FETCH clauses based on the includes parameter.

Service

import jakarta.persistence.EntityGraph;
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import java.util.Map;

@Service
public class ProductService {

    private final EntityManager entityManager;

    public ProductService(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    public Product findProductWithIncludes(Long id, String includes) {
        // Basic example: Create a simple EntityGraph dynamically
        EntityGraph<Product> graph = entityManager.createEntityGraph(Product.class);

        if (includes != null && includes.contains("category")) {
             graph.addAttributeNodes("category"); // Eagerly load 'category'
        }
        if (includes != null && includes.contains("reviews")) {
            // Add sub-graphs or other logic for complex relationships
             graph.addAttributeNodes("reviews"); 
        }

        // Fetch the entity using the dynamic EntityGraph
        return entityManager.find(Product.class, id, Map.of("jakarta.persistence.fetchgraph", graph));
    }
}

Note:
The above JPA example is simplified version.
In production, consider using QueryDSL or Specification to build dynamic JOIN FETCH queries for more complex scenarios.


Request Flow

Here’s how Client-Driven Filtering fits into a typical request flow:

sequenceDiagram
    participant C as Client
    participant SB as Spring Boot API
    participant DB as Database (JPA)

    C->>SB: GET /products/1?fields=name,price&includes=category
    activate SB
    SB-->>DB: Dynamic Query (e.g., SELECT name, price, category details)
    activate DB
    DB-->>SB: Full Entity/DTO with Eager Loaded Category
    deactivate DB
    SB->>SB: Serialization (Jackson applies 'fields' filter)
    SB-->>C: Response (JSON with only name, price, and category)
    deactivate SB

Use Case

ClientRequestDescription
Mobile List View/products?fields=id,name,priceNeeds minimal data for a scrolling list of products. Saves bandwidth by omitting descriptions, images, etc.
Web Detail Page/products?fields=*,description&includes=reviews,sellerNeeds all core fields (*) plus description, and requires related entities like reviews and seller details in a single call.
Internal Service/products?fields=id,internalSkuAn inventory service only needs identifiers for its processing, filtering out public-facing data.

Best Practices

For maximum clarity and adoption, I suggest adopting a standard, well-documented approach like the GraphQL data-fetching model or adhering to the JSON:API standard for complex relationship inclusion/exclusion.

Adoption of a Standard:

  • GraphQL: Completely eliminates over-fetching by design, allowing the client to specify the exact shape of the response in the query itself.
  • Recommendation: If performance and flexible data retrieval is a core requirement across many microservices, consider migrating key read operations to a GraphQL layer (e.g., a Gateway or BFF - Backend For Frontend) that aggregates data from your underlying Spring Boot microservices.