Skip to content

Advanced Usage

philipp-gatzka edited this page Dec 26, 2025 · 1 revision

Advanced Usage

Advanced patterns and features for using Graphite effectively.

Custom Scalars

GraphQL custom scalars map to Java types. Graphite provides built-in support for common scalars and allows you to add your own.

Built-in Scalar Coercings

GraphQL Scalar Java Type Coercing Class
DateTime java.time.Instant DateTimeCoercing
Date java.time.LocalDate DateCoercing
Time java.time.LocalTime TimeCoercing
BigDecimal java.math.BigDecimal BigDecimalCoercing
BigInteger java.math.BigInteger BigIntegerCoercing
Long java.lang.Long LongCoercing
UUID java.util.UUID UuidCoercing
JSON com.fasterxml.jackson.databind.JsonNode JsonCoercing
Void java.lang.Void VoidCoercing

Configuring Scalar Mappings

In Gradle:

graphite {
    scalars = mapOf(
        "DateTime" to "java.time.Instant",
        "Date" to "java.time.LocalDate",
        "Money" to "java.math.BigDecimal"
    )
}

In Maven:

<scalars>
    <DateTime>java.time.Instant</DateTime>
    <Date>java.time.LocalDate</Date>
    <Money>java.math.BigDecimal</Money>
</scalars>

Custom Scalar Coercing

Implement ScalarCoercing for custom serialization/deserialization:

import io.github.graphite.scalar.ScalarCoercing;

public class MoneyCoercing implements ScalarCoercing<Money> {

    public static final MoneyCoercing INSTANCE = new MoneyCoercing();

    @Override
    public Money deserialize(Object value) {
        if (value instanceof String s) {
            return Money.parse(s);
        }
        if (value instanceof Map<?, ?> map) {
            BigDecimal amount = new BigDecimal(map.get("amount").toString());
            String currency = map.get("currency").toString();
            return Money.of(amount, currency);
        }
        throw new IllegalArgumentException("Cannot deserialize Money from: " + value);
    }

    @Override
    public Object serialize(Money value) {
        return Map.of(
            "amount", value.getAmount().toString(),
            "currency", value.getCurrency().getCurrencyCode()
        );
    }
}

Register your coercing:

import io.github.graphite.scalar.ScalarRegistry;

ScalarRegistry registry = ScalarRegistry.builder()
    .register("Money", MoneyCoercing.INSTANCE)
    .build();

GraphiteClient client = GraphiteClient.builder()
    .endpoint(uri)
    .scalarRegistry(registry)
    .build();

Request Interceptors

Interceptors allow you to modify requests before they are sent.

Adding Dynamic Headers

import io.github.graphite.interceptor.RequestInterceptor;

public class AuthInterceptor implements RequestInterceptor {

    private final TokenProvider tokenProvider;

    public AuthInterceptor(TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    @Override
    public HttpRequest intercept(HttpRequest request) {
        String token = tokenProvider.getAccessToken();
        return request.withHeader("Authorization", "Bearer " + token);
    }
}

Register the interceptor:

GraphiteClient client = GraphiteClient.builder()
    .endpoint(uri)
    .requestInterceptor(new AuthInterceptor(tokenProvider))
    .build();

Logging Requests

public class LoggingInterceptor implements RequestInterceptor {

    private static final Logger log = LoggerFactory.getLogger(LoggingInterceptor.class);

    @Override
    public HttpRequest intercept(HttpRequest request) {
        log.debug("GraphQL Request: {} {}", request.method(), request.uri());
        log.trace("Request body: {}", request.body());
        return request;
    }
}

Response Interceptors

Process responses after they are received:

import io.github.graphite.interceptor.ResponseInterceptor;

public class MetricsInterceptor implements ResponseInterceptor {

    private final MeterRegistry meterRegistry;

    @Override
    public HttpResponse intercept(HttpResponse response, HttpRequest request) {
        meterRegistry.counter("graphql.responses",
            "status", String.valueOf(response.statusCode()))
            .increment();
        return response;
    }
}

Error Handling

Handling GraphQL Errors

var response = client.execute(query);

if (response.hasErrors()) {
    for (GraphQLError error : response.errors()) {
        log.error("GraphQL error: {} at {}",
            error.message(),
            error.locations());

        // Check error extensions for error codes
        if (error.extensions() != null) {
            String code = (String) error.extensions().get("code");
            if ("UNAUTHORIZED".equals(code)) {
                throw new UnauthorizedException(error.message());
            }
        }
    }
}

// Or use getDataOrThrow() to throw on errors
UserDTO user = response.getDataOrThrow();

Exception Hierarchy

GraphiteException (base)
├── GraphiteClientException (client-side errors)
│   ├── GraphiteConnectionException (network failures)
│   ├── GraphiteTimeoutException (timeouts)
│   └── GraphiteRateLimitException (rate limited)
└── GraphiteServerException (server-side errors)
    └── GraphiteGraphQLException (GraphQL errors in response)

Custom Error Handling

try {
    client.execute(query);
} catch (GraphiteConnectionException e) {
    log.error("Network error: {}", e.getMessage());
    // Implement fallback logic
} catch (GraphiteTimeoutException e) {
    log.error("Timeout: {} (type: {})", e.getMessage(), e.getTimeoutType());
    // CONNECT, READ, or REQUEST timeout
} catch (GraphiteRateLimitException e) {
    log.warn("Rate limited, retry after: {}", e.getRetryAfter());
    // Wait and retry
} catch (GraphiteGraphQLException e) {
    log.error("GraphQL errors: {}", e.getErrors());
    // Handle application-level errors
}

Observability

Metrics

Graphite exposes metrics via Micrometer:

// Request metrics
graphite.client.requests{operation=GetUser, status=success}
graphite.client.requests{operation=GetUser, status=error}
graphite.client.request.duration{operation=GetUser}

// Retry metrics
graphite.client.retry.attempts{operation=GetUser}
graphite.client.retry.exhausted{operation=GetUser}

// HTTP connection pool metrics
graphite.http.connections.active{client=default}
graphite.http.connections.pending{client=default}
graphite.http.connections.max{client=default}
graphite.http.connections.total{client=default}
graphite.http.connections.acquired{client=default}

Tracing

Graphite propagates trace context automatically when Micrometer Tracing is on the classpath:

# application.yml
spring:
  application:
    name: my-service

management:
  tracing:
    enabled: true
    sampling:
      probability: 1.0

The following headers are propagated:

  • traceparent (W3C Trace Context)
  • tracestate (W3C Trace Context)
  • X-B3-TraceId, X-B3-SpanId (Zipkin B3)

Logging with MDC

Graphite adds context to MDC for structured logging:

// Available MDC keys during request execution:
// - graphite.operation: Operation name
// - graphite.requestId: Unique request ID
// - graphite.correlationId: Correlation ID (if provided)

Configure in logback:

<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{graphite.operation}] - %msg%n</pattern>

Testing Patterns

Testing with GraphiteMockServer

@Test
void shouldHandleMultipleOperations() {
    try (GraphiteMockServer server = GraphiteMockServer.create()) {
        // Stub multiple operations
        server.stubQuery("GetUser", Map.of("id", "1", "name", "Alice"));
        server.stubQuery("GetPosts", List.of(
            Map.of("id", "1", "title", "Post 1"),
            Map.of("id", "2", "title", "Post 2")
        ));

        // Execute operations
        var user = client.execute(getUserQuery).data();
        var posts = client.execute(getPostsQuery).data();

        // Verify
        server.verify("GetUser", 1);
        server.verify("GetPosts", 1);
    }
}

Testing Error Scenarios

@Test
void shouldHandleGraphQLErrors() {
    try (GraphiteMockServer server = GraphiteMockServer.create()) {
        server.stubError("GetUser",
            GraphQLError.builder()
                .message("User not found")
                .path(List.of("user"))
                .extensions(Map.of("code", "NOT_FOUND"))
                .build());

        var response = client.execute(getUserQuery);

        assertThat(response.hasErrors()).isTrue();
        assertThat(response.errors().get(0).message()).isEqualTo("User not found");
    }
}

@Test
void shouldHandleHttpErrors() {
    try (GraphiteMockServer server = GraphiteMockServer.create()) {
        server.stubHttpError("GetUser", 500);

        assertThatThrownBy(() -> client.execute(getUserQuery))
            .isInstanceOf(GraphiteServerException.class);
    }
}

Testing Timeouts

@Test
void shouldTimeout() {
    try (GraphiteMockServer server = GraphiteMockServer.create()) {
        server.stubWithDelay("GetUser", 5000, Map.of("id", "1"));

        GraphiteClient client = GraphiteClient.builder()
            .endpoint(URI.create(server.getUrl()))
            .requestTimeout(Duration.ofMillis(100))
            .build();

        assertThatThrownBy(() -> client.execute(getUserQuery))
            .isInstanceOf(GraphiteTimeoutException.class);
    }
}

Performance Tips

Connection Pooling

Use connection pooling for high-throughput scenarios:

graphite:
  connection-pool:
    max-connections: 100
    idle-timeout: 60s

Async Execution

Use async execution for non-blocking operations:

List<CompletableFuture<UserDTO>> futures = userIds.stream()
    .map(id -> client.executeAsync(getUserQuery(id))
        .thenApply(GraphiteResponse::getDataOrThrow))
    .toList();

List<UserDTO> users = futures.stream()
    .map(CompletableFuture::join)
    .toList();

Rate Limiting

Enable rate limiting to prevent server overload:

graphite:
  rate-limit:
    enabled: true
    requests-per-second: 50
    burst-capacity: 75

See Also