Complete guide for using the GraphQL client helper in the Firefly Common Client Library.
- Overview
- When to Use GraphQL Client
- Quick Start
- Configuration
- Executing Queries
- Executing Mutations
- Working with Variables
- Error Handling
- Advanced Usage
- 🚀 Batch Operations
- 💾 Query Caching
- 🔄 Automatic Retry Logic
- Best Practices
- Complete Examples
The GraphQLClientHelper provides a production-ready, enterprise-grade API for interacting with GraphQL endpoints with advanced features like retry logic, query caching, and batch operations.
Key Features:
- ✅ Reactive programming with
Mono<T>andFlux<T> - ✅ Type-safe response handling
- ✅ Variable binding support
- ✅ Custom headers per request
- ✅ Configurable timeouts
- ✅ Automatic error parsing
- ✅ Data extraction utilities
- ✅ NEW: Automatic retry with exponential backoff
- ✅ NEW: Query caching for improved performance
- ✅ NEW: Batch operations support
- ✅ NEW: Builder pattern for advanced configuration
- ✅ NEW: Java Time API support (LocalDate, LocalDateTime, etc.)
- ✅ NEW: Smart error handling with retryable error detection
Note: For production applications with complex GraphQL needs, consider using dedicated frameworks like:
- Spring for GraphQL
- Netflix DGS Framework
- GraphQL Java
- You need to consume third-party GraphQL APIs
- You want flexible data fetching with queries
- You need to perform mutations on GraphQL endpoints
- You're building a GraphQL client for microservices
- You want reactive, non-blocking GraphQL operations
- You need GraphQL subscriptions (use WebSocket helper + GraphQL)
- You're building a GraphQL server (use Spring for GraphQL)
- You need advanced features like schema introspection, code generation
- You require federation or stitching capabilities
import org.fireflyframework.client.graphql.GraphQLClientHelper;
import reactor.core.publisher.Mono;
// Create GraphQL client
GraphQLClientHelper graphql = new GraphQLClientHelper("https://api.example.com/graphql");
// Execute a simple query
String query = """
query {
users {
id
name
email
}
}
""";
Mono<GraphQLResponse<Object>> response = graphql.query(query);// Define your response type
public class User {
private String id;
private String name;
private String email;
// getters and setters
}
// Execute query with type extraction
String query = """
query GetUsers {
users {
id
name
email
}
}
""";
Mono<List<User>> users = graphql.query(query, null, "users", new TypeReference<List<User>>() {});// Simple configuration
GraphQLClientHelper graphql = new GraphQLClientHelper("https://api.example.com/graphql");import java.time.Duration;
import java.util.Map;
// With custom timeout and headers
Duration timeout = Duration.ofSeconds(60);
Map<String, String> defaultHeaders = Map.of(
"Authorization", "Bearer your-token-here",
"X-API-Version", "v1",
"X-Client-Name", "my-app"
);
GraphQLClientHelper graphql = new GraphQLClientHelper(
"https://api.example.com/graphql",
timeout,
defaultHeaders
);import org.fireflyframework.client.graphql.GraphQLClientHelper.GraphQLConfig;
import java.time.Duration;
// Enterprise-grade configuration with all features
GraphQLConfig config = GraphQLConfig.builder()
.timeout(Duration.ofMinutes(2)) // 2 minute timeout for complex queries
.enableRetry(true) // Enable automatic retry on failures
.maxRetries(3) // Retry up to 3 times
.retryBackoff(Duration.ofSeconds(1)) // 1 second initial backoff (exponential)
.enableQueryCache(true) // Cache query results for performance
.defaultHeader("Authorization", "Bearer token")
.defaultHeader("X-Client-Name", "my-service")
.defaultHeader("X-API-Version", "v2")
.build();
GraphQLClientHelper graphql = new GraphQLClientHelper(
"https://api.example.com/graphql",
config
);| Option | Default | Description |
|---|---|---|
timeout |
30 seconds | Maximum time to wait for a response |
enableRetry |
false | Enable automatic retry on 5xx errors and timeouts |
maxRetries |
3 | Maximum number of retry attempts |
retryBackoff |
500ms | Initial backoff duration (uses exponential backoff) |
enableQueryCache |
false | Cache query results (only for queries without variables/headers) |
defaultHeaders |
empty | Headers included in all requests |
Retry Behavior:
- Retries on: 5xx server errors, 429 Too Many Requests, timeouts, connection errors
- Does NOT retry on: 4xx client errors (except 429), GraphQL errors in response
- Uses exponential backoff: 1s, 2s, 4s, 8s, etc.
import org.fireflyframework.client.oauth2.OAuth2ClientHelper;
// Setup OAuth2
OAuth2ClientHelper oauth2 = new OAuth2ClientHelper(
"https://auth.example.com/oauth/token",
"client-id",
"client-secret"
);
// Get token and create GraphQL client
oauth2.getClientCredentialsToken().flatMap(token -> {
Map<String, String> headers = Map.of("Authorization", "Bearer " + token);
GraphQLClientHelper graphql = new GraphQLClientHelper(
"https://api.example.com/graphql",
Duration.ofSeconds(30),
headers
);
return graphql.query("{ users { id name } }");
}).subscribe();String query = """
query {
currentUser {
id
name
email
}
}
""";
graphql.query(query)
.subscribe(response -> {
if (!response.hasErrors()) {
System.out.println("Data: " + response.getData());
}
});String query = """
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
title
content
}
}
}
""";
Map<String, Object> variables = Map.of("id", "123");
graphql.query(query, variables)
.subscribe(response -> {
System.out.println("User: " + response.getData());
});String query = """
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
""";
Map<String, Object> variables = Map.of("id", "123");
// Extract directly to User object
Mono<User> user = graphql.query(query, variables, "user", User.class);
user.subscribe(u -> System.out.println("User name: " + u.getName()));String query = """
query {
data {
user {
id
name
}
}
}
""";
// Navigate nested path: data.user
Mono<User> user = graphql.query(query, null, "data.user", User.class);String mutation = """
mutation {
createUser(input: {
name: "John Doe"
email: "john@example.com"
}) {
id
name
email
}
}
""";
graphql.mutate(mutation)
.subscribe(response -> {
System.out.println("Created user: " + response.getData());
});String mutation = """
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
createdAt
}
}
""";
Map<String, Object> variables = Map.of(
"input", Map.of(
"name", "John Doe",
"email", "john@example.com",
"role", "USER"
)
);
graphql.mutate(mutation, variables)
.subscribe(response -> {
System.out.println("User created: " + response.getData());
});String mutation = """
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
updatedAt
}
}
""";
Map<String, Object> variables = Map.of(
"id", "123",
"input", Map.of("name", "Jane Doe")
);
Mono<User> updatedUser = graphql.mutate(mutation, variables, "updateUser", User.class);
updatedUser.subscribe(user -> {
System.out.println("Updated: " + user.getName());
});Map<String, Object> variables = Map.of(
"id", "123",
"name", "John Doe",
"active", true
);Map<String, Object> variables = Map.of(
"input", Map.of(
"user", Map.of(
"name", "John Doe",
"email", "john@example.com",
"age", 30
),
"preferences", Map.of(
"theme", "dark",
"notifications", true
),
"tags", List.of("developer", "java", "graphql")
)
);Map<String, Object> variables = new HashMap<>();
variables.put("id", "123");
variables.put("name", null); // Explicitly null
variables.put("email", "john@example.com");graphql.query(query, variables)
.subscribe(response -> {
if (response.hasErrors()) {
for (GraphQLError error : response.getErrors()) {
System.err.println("GraphQL Error: " + error.getMessage());
System.err.println("Path: " + Arrays.toString(error.getPath()));
}
} else {
System.out.println("Success: " + response.getData());
}
});graphql.query(query, variables, "user", User.class)
.doOnError(error -> {
if (error instanceof GraphQLException) {
System.err.println("GraphQL error: " + error.getMessage());
}
})
.onErrorReturn(new User()) // Fallback user
.subscribe(user -> {
System.out.println("User: " + user.getName());
});graphql.query(query, variables)
.retry(3) // Retry up to 3 times
.subscribe(response -> {
System.out.println("Response: " + response.getData());
});Map<String, String> customHeaders = Map.of(
"X-Request-ID", UUID.randomUUID().toString(),
"X-Correlation-ID", "correlation-123"
);
graphql.execute(query, variables, customHeaders)
.subscribe(response -> {
System.out.println("Response: " + response.getData());
});Mono<User> userMono = graphql.query(userQuery, userVars, "user", User.class);
Mono<List<Post>> postsMono = graphql.query(postsQuery, postsVars, "posts",
new TypeReference<List<Post>>() {});
Mono.zip(userMono, postsMono)
.subscribe(tuple -> {
User user = tuple.getT1();
List<Post> posts = tuple.getT2();
System.out.println("User: " + user.getName() + ", Posts: " + posts.size());
});// Chain GraphQL operations
graphql.query(getUserQuery, Map.of("id", "123"), "user", User.class)
.flatMap(user -> {
// Use user data to fetch posts
Map<String, Object> vars = Map.of("userId", user.getId());
return graphql.query(getPostsQuery, vars, "posts", new TypeReference<List<Post>>() {});
})
.subscribe(posts -> {
System.out.println("User's posts: " + posts.size());
});Execute multiple GraphQL queries in parallel for improved performance.
import org.fireflyframework.client.graphql.GraphQLClientHelper.GraphQLRequest;
import java.util.List;
// Build multiple requests
List<GraphQLRequest> requests = List.of(
GraphQLRequest.builder()
.query("query GetUser($id: ID!) { user(id: $id) { id name } }")
.variable("id", "123")
.header("X-Request-ID", "req-1")
.build(),
GraphQLRequest.builder()
.query("query GetPosts($limit: Int!) { posts(limit: $limit) { id title } }")
.variable("limit", 10)
.header("X-Request-ID", "req-2")
.build(),
GraphQLRequest.builder()
.query("query GetComments { comments { id text } }")
.build()
);
// Execute all requests in parallel
graphql.executeBatch(requests)
.collectList()
.subscribe(responses -> {
System.out.println("Received " + responses.size() + " responses");
responses.forEach(response -> {
if (!response.hasErrors()) {
System.out.println("Data: " + response.getData());
}
});
});graphql.executeBatch(requests)
.doOnNext(response -> {
if (response.hasErrors()) {
System.err.println("Query failed: " + response.getErrors()[0].getMessage());
} else {
System.out.println("Query succeeded: " + response.getData());
}
})
.collectList()
.subscribe(
responses -> System.out.println("All queries completed"),
error -> System.err.println("Batch failed: " + error.getMessage())
);Improve performance by caching query results.
GraphQLConfig config = GraphQLConfig.builder()
.enableQueryCache(true)
.build();
GraphQLClientHelper graphql = new GraphQLClientHelper(
"https://api.example.com/graphql",
config
);- ✅ Cached: Queries without variables or custom headers
- ❌ NOT Cached: Queries with variables or custom headers
- Cache key is based on query string hash
- Cache is in-memory and per-instance
String query = "query { users { id name } }";
// First call - hits the server
graphql.query(query).subscribe(response -> {
System.out.println("First call: " + response.getData());
});
// Second call - returns cached result (no server hit)
graphql.query(query).subscribe(response -> {
System.out.println("Second call (cached): " + response.getData());
});
// Clear cache when needed
graphql.clearCache();
System.out.println("Cache size: " + graphql.getCacheSize());// Check cache size
int size = graphql.getCacheSize();
System.out.println("Cached queries: " + size);
// Clear cache
graphql.clearCache();
// Cache is automatically cleared when:
// - clearCache() is called
// - Instance is garbage collectedBest Practices:
- Use caching for static/reference data queries
- Don't cache user-specific or frequently changing data
- Clear cache periodically in long-running applications
- Monitor cache size in production
Handle transient failures automatically with exponential backoff.
GraphQLConfig config = GraphQLConfig.builder()
.enableRetry(true)
.maxRetries(3)
.retryBackoff(Duration.ofSeconds(1))
.build();
GraphQLClientHelper graphql = new GraphQLClientHelper(
"https://api.example.com/graphql",
config
);The client automatically retries on:
- ✅ 5xx Server Errors (500, 502, 503, 504)
- ✅ 429 Too Many Requests
- ✅ Timeout exceptions
- ✅ Connection errors
Does NOT retry on:
- ❌ 4xx Client Errors (except 429)
- ❌ GraphQL errors in response body
- ❌ Invalid queries
Attempt 1: Immediate
Attempt 2: Wait 1 second (backoff)
Attempt 3: Wait 2 seconds (exponential)
Attempt 4: Wait 4 seconds (exponential)
GraphQLConfig config = GraphQLConfig.builder()
.enableRetry(true)
.maxRetries(5)
.retryBackoff(Duration.ofMillis(500))
.build();
GraphQLClientHelper graphql = new GraphQLClientHelper(
"https://unreliable-api.example.com/graphql",
config
);
// This will automatically retry up to 5 times on failures
graphql.query(query, variables, "data", MyData.class)
.doOnError(error -> {
System.err.println("All retries exhausted: " + error.getMessage());
})
.subscribe(data -> {
System.out.println("Success after retries: " + data);
});Best Practices:
- Enable retry for production environments
- Use reasonable maxRetries (3-5)
- Set appropriate backoff duration (500ms-2s)
- Monitor retry metrics in production
- Combine with circuit breaker for better resilience
❌ Bad:
String query = "query { user(id: \"" + userId + "\") { name } }";✅ Good:
String query = "query GetUser($id: ID!) { user(id: $id) { name } }";
Map<String, Object> variables = Map.of("id", userId);❌ Bad:
graphql.query(query).subscribe(response -> {
Map<String, Object> data = (Map<String, Object>) response.getData();
String name = (String) data.get("name"); // Unsafe casting
});✅ Good:
graphql.query(query, variables, "user", User.class)
.subscribe(user -> {
String name = user.getName(); // Type-safe
});❌ Bad:
graphql.query(query).subscribe(response -> {
// Assumes no errors
processData(response.getData());
});✅ Good:
graphql.query(query).subscribe(response -> {
if (response.hasErrors()) {
handleErrors(response.getErrors());
} else {
processData(response.getData());
}
});❌ Bad (Development only):
GraphQLClientHelper graphql = new GraphQLClientHelper("https://api.example.com/graphql");✅ Good (Production-ready):
GraphQLConfig config = GraphQLConfig.builder()
.timeout(Duration.ofMinutes(2))
.enableRetry(true)
.maxRetries(3)
.enableQueryCache(true) // For static data
.defaultHeader("Authorization", "Bearer " + token)
.build();
GraphQLClientHelper graphql = new GraphQLClientHelper(
"https://api.example.com/graphql",
config
);❌ Bad (Sequential):
graphql.query(query1).subscribe(r1 -> {
graphql.query(query2).subscribe(r2 -> {
graphql.query(query3).subscribe(r3 -> {
// Process results
});
});
});✅ Good (Parallel):
List<GraphQLRequest> requests = List.of(
GraphQLRequest.builder().query(query1).build(),
GraphQLRequest.builder().query(query2).build(),
GraphQLRequest.builder().query(query3).build()
);
graphql.executeBatch(requests)
.collectList()
.subscribe(responses -> {
// Process all results
});String userFragment = """
fragment UserFields on User {
id
name
email
createdAt
}
""";
String query = """
query GetUser($id: ID!) {
user(id: $id) {
...UserFields
posts {
title
}
}
}
""" + userFragment;// For quick queries
GraphQLClientHelper quickClient = new GraphQLClientHelper(
endpoint,
Duration.ofSeconds(5),
headers
);
// For complex queries
GraphQLClientHelper complexClient = new GraphQLClientHelper(
endpoint,
Duration.ofSeconds(60),
headers
);❌ Bad:
public Mono<User> getUser(String id) {
GraphQLClientHelper graphql = new GraphQLClientHelper(endpoint); // New instance each time
return graphql.query(query, Map.of("id", id), "user", User.class);
}✅ Good:
@Component
public class UserService {
private final GraphQLClientHelper graphql;
public UserService() {
this.graphql = new GraphQLClientHelper(endpoint); // Reuse instance
}
public Mono<User> getUser(String id) {
return graphql.query(query, Map.of("id", id), "user", User.class);
}
}import org.fireflyframework.client.graphql.GraphQLClientHelper;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.Map;
public class GitHubGraphQLClient {
private final GraphQLClientHelper graphql;
public GitHubGraphQLClient(String token) {
Map<String, String> headers = Map.of(
"Authorization", "Bearer " + token
);
this.graphql = new GraphQLClientHelper(
"https://api.github.com/graphql",
Duration.ofSeconds(30),
headers
);
}
public Mono<Repository> getRepository(String owner, String name) {
String query = """
query GetRepository($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
id
name
description
stargazerCount
forkCount
url
createdAt
updatedAt
}
}
""";
Map<String, Object> variables = Map.of(
"owner", owner,
"name", name
);
return graphql.query(query, variables, "repository", Repository.class);
}
public Mono<User> getViewer() {
String query = """
query {
viewer {
login
name
email
bio
avatarUrl
repositories(first: 10) {
totalCount
nodes {
name
stargazerCount
}
}
}
}
""";
return graphql.query(query, null, "viewer", User.class);
}
public Mono<Issue> createIssue(String repositoryId, String title, String body) {
String mutation = """
mutation CreateIssue($input: CreateIssueInput!) {
createIssue(input: $input) {
issue {
id
number
title
body
url
createdAt
}
}
}
""";
Map<String, Object> variables = Map.of(
"input", Map.of(
"repositoryId", repositoryId,
"title", title,
"body", body
)
);
return graphql.mutate(mutation, variables, "createIssue.issue", Issue.class);
}
// DTOs
public static class Repository {
private String id;
private String name;
private String description;
private int stargazerCount;
private int forkCount;
private String url;
private String createdAt;
private String updatedAt;
// getters and setters
}
public static class User {
private String login;
private String name;
private String email;
private String bio;
private String avatarUrl;
// getters and setters
}
public static class Issue {
private String id;
private int number;
private String title;
private String body;
private String url;
private String createdAt;
// getters and setters
}
}import org.fireflyframework.client.graphql.GraphQLClientHelper;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
public class ProductCatalogClient {
private final GraphQLClientHelper graphql;
public ProductCatalogClient(String endpoint) {
this.graphql = new GraphQLClientHelper(endpoint);
}
public Mono<List<Product>> searchProducts(String searchTerm, int limit) {
String query = """
query SearchProducts($search: String!, $limit: Int!) {
products(search: $search, first: $limit) {
edges {
node {
id
name
description
price
currency
imageUrl
inStock
category {
id
name
}
}
}
}
}
""";
Map<String, Object> variables = Map.of(
"search", searchTerm,
"limit", limit
);
return graphql.query(query, variables, "products.edges",
new TypeReference<List<ProductEdge>>() {})
.map(edges -> edges.stream()
.map(edge -> edge.node)
.collect(Collectors.toList()));
}
public Mono<Order> createOrder(String userId, List<OrderItem> items) {
String mutation = """
mutation CreateOrder($input: CreateOrderInput!) {
createOrder(input: $input) {
order {
id
orderNumber
status
totalAmount
currency
createdAt
items {
productId
quantity
price
}
}
}
}
""";
Map<String, Object> variables = Map.of(
"input", Map.of(
"userId", userId,
"items", items.stream()
.map(item -> Map.of(
"productId", item.getProductId(),
"quantity", item.getQuantity()
))
.collect(Collectors.toList())
)
);
return graphql.mutate(mutation, variables, "createOrder.order", Order.class);
}
// DTOs
public static class Product {
private String id;
private String name;
private String description;
private double price;
private String currency;
private String imageUrl;
private boolean inStock;
private Category category;
// getters and setters
}
public static class Category {
private String id;
private String name;
// getters and setters
}
public static class ProductEdge {
private Product node;
// getters and setters
}
public static class Order {
private String id;
private String orderNumber;
private String status;
private double totalAmount;
private String currency;
private String createdAt;
private List<OrderItemResponse> items;
// getters and setters
}
public static class OrderItem {
private String productId;
private int quantity;
// getters and setters
}
public static class OrderItemResponse {
private String productId;
private int quantity;
private double price;
// getters and setters
}
}✅ Query Execution: Full support for GraphQL queries
✅ Mutation Execution: Create, update, delete operations
✅ Variables: Type-safe variable binding
✅ Error Handling: Automatic GraphQL error parsing
✅ Data Extraction: Navigate and extract nested data
✅ Custom Headers: Per-request header customization
✅ Timeouts: Configurable request timeouts
✅ Reactive: Full Mono<T> and Flux<T> support
❌ Subscriptions: Use WebSocket helper for real-time subscriptions ❌ Schema Introspection: Use dedicated GraphQL tools ❌ Code Generation: Use GraphQL code generators ❌ Federation: Use Apollo Federation or similar ❌ Batching: Implement custom batching logic ❌ Caching: Implement custom caching strategy
For Production: Consider using Spring for GraphQL or Netflix DGS Framework for advanced GraphQL features.
Next Steps: