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
- What is Client-Driven filtering ?
- Table of Contents
- Purpose and Benefits
- Implementation
- Use Case
- Best Practices
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.
| Aspect | Short Description | Detailed Benefit |
|---|---|---|
Purpose | Improve Network Efficiency and Application Performance | Achieved by minimizing the overall data payload size transferred between client and server. |
Bandwidth Reduction | Decreases data transfer volume. | Especially beneficial for mobile clients or connections with high latency and limited bandwidth. |
Faster Response Time | Quicker processing on both ends | Smaller payloads reduce the time required for serialization/deserialization processes on the server and client. |
Reduced Server Load | Lower resource utilization | Less data to process in the application layer and potentially less strain on the database if filtering influences the data retrieval query. |
API Flexibility | Single, robust endpoint for diverse clients | Allows 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 dynamicJOIN FETCHqueries 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
| Client | Request | Description |
|---|---|---|
| Mobile List View | /products?fields=id,name,price | Needs minimal data for a scrolling list of products. Saves bandwidth by omitting descriptions, images, etc. |
| Web Detail Page | /products?fields=*,description&includes=reviews,seller | Needs all core fields (*) plus description, and requires related entities like reviews and seller details in a single call. |
| Internal Service | /products?fields=id,internalSku | An 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.