Service Communication

Service-to-service communication for Spring Boot microservices

Spring Middleware provides a platform model for service-to-service communication in Spring Boot microservices. Declarative HTTP clients built on top of WebClient interact with Registry-discovered services, propagate request context, handle structured remote errors, and apply resilience patterns such as circuit breakers and bulkheads.

Declarative HTTP clients Service discovery WebClient integration Request ID and span propagation Structured error handling Circuit breaker and bulkhead

What communication means here

Service-to-service communication in Spring Middleware is not based on ad hoc WebClient wiring scattered across services. The platform provides declarative clients that describe the remote contract and centralizes cross-cutting concerns such as service resolution, context propagation, structured error mapping, and resilience behavior.

  • Keep service contracts explicit at interface level
  • Remove repeated HTTP client setup from each service
  • Preserve request context across service boundaries
  • Keep remote failures structured and traceable

Platform role vs infrastructure role

Spring Middleware resolves logical service identity and platform metadata, but it is not responsible for client-side load balancing. In Kubernetes-based deployments, the framework communicates through the service endpoint exposed by the platform, while instance-level distribution is handled by Kubernetes.

  • Spring Middleware: contract, topology awareness, tracing, errors, resilience.
  • Registry: logical service metadata and discoverability.
  • Kubernetes Service: stable endpoint for the target cluster.
  • Kubernetes: pod-level load distribution.

High-level communication view

Application code calls a declarative client, the platform resolves the logical target service, prepares the outbound request with propagated context, and maps the response back into the declared Java contract.

Caller Service Declarative Client Proxy Target Service Spring Middleware Communication Layer service resolution • context propagation • structured errors retries • bulkhead • circuit breaker • connection tuning Kubernetes Service endpoint

Declarative client model

A remote service is described as a Java interface. The interface expresses the HTTP contract, while the platform provides the proxy implementation and runtime behavior.

Basic client example

@MiddlewareClient(service = "product")
public interface ProductsApi {

    @PostMapping("/api/v1/products/bulk")
    List<ProductDto> createProducts(
        ProductBulkCreateRequestDto request
    );

}

What the client declaration provides

  • service = "product" identifies the logical target service.
  • The method mapping defines path, HTTP method, request body, and return type.
  • The framework creates the proxy and executes the remote call through WebClient.
  • The caller depends on a stable contract instead of hand-written HTTP plumbing.

Runtime call flow

The communication layer keeps the remote call pipeline explicit: contract invocation, service resolution, request construction, execution, and response mapping.

01

Invoke the client interface

Application code calls a method on a declarative client proxy.

02

Resolve the logical target

The platform identifies the target service from client metadata and Registry information.

03

Build and execute the request

The outbound request is created with headers, payload, and connection settings, then executed through WebClient.

What happens inside the platform

  • Read client metadata and mapping annotations
  • Resolve the logical target service through Registry-backed metadata
  • Build the target URL using the service endpoint and method mapping
  • Propagate request context headers
  • Serialize the request body and execute the call
  • Deserialize the response or map the remote failure

Important deployment assumption

In Kubernetes deployments, the communication layer targets the stable service endpoint associated with the logical cluster. The framework does not implement its own node selection strategy or client-side balancing policy for pods.

  • Logical service: expressed by the client contract.
  • Service endpoint: provided by the deployment platform.
  • Load balancing: delegated to Kubernetes networking.

Request context propagation

Every remote call carries the platform request context so that logs, errors, and cross-service execution can be correlated consistently.

Headers propagated across services

  • X-Request-ID: end-to-end request identifier across the full chain.
  • X-Span-ID: local span identifier for the current outgoing hop.

If the incoming request already contains these headers, they are reused. Otherwise, the platform generates them at the boundary.

Why it matters

  • Correlation: follow one business request across multiple services.
  • Debugging: connect logs, errors, and remote calls quickly.
  • Consistency: use the same tracing semantics for REST and GraphQL paths.
Service A requestId = 4C7F... spanId = A12F... Service B requestId = 4C7F... spanId = B992... Service C requestId = 4C7F... spanId = C11A...

Structured remote error model

Remote failures are mapped into a consistent platform error structure instead of being treated as raw HTTP noise. This keeps downstream failures explicit and traceable.

Error payload example

{
  "statusCode": 404,
  "statusMessage": "Not Found",
  "code": "PRODUCT:NOT_FOUND",
  "message": "Product not found",
  "extensions": {
    "requestId": "F4D29AAFE7FC4844A1FF8794F186B102",
    "span": [
      {
        "service": "catalog",
        "method": "replaceProducts",
        "httpStatus": 404
      }
    ]
  }
}

How remote failures are surfaced

A failed remote call can be mapped into a structured exception such as RemoteServerException, carrying status, code, message, request metadata, remote service information, and call-chain context.

  • HTTP status and error message remain visible
  • Domain error codes can be preserved across service boundaries
  • Request and span identifiers remain available for correlation
  • Applications can translate or rethrow structured remote exceptions

Relationship with GraphQL

The communication model is shared across REST and GraphQL-based flows. Service-to-service calls still use the same request context propagation and the same structured error semantics.

Shared context model

GraphQL paths still propagate X-Request-ID and X-Span-ID across downstream service calls.

Shared error semantics

Centralized exception handling maps platform errors into GraphQL error responses with explicit codes and metadata extensions.

Same communication foundation

Declarative clients, remote failures, and request correlation behave consistently regardless of whether the entry point is REST or GraphQL.

Client resilience and connection control

Declarative clients can be protected with circuit breakers, bulkheads, retries, timeouts, and connection parameters so that downstream failures do not turn into uncontrolled cascading problems.

Circuit breaker

Remote calls can run under Resilience4j circuit breakers that open when failure thresholds are exceeded and recover through half-open transitions.

Bulkhead

Per-cluster semaphores limit concurrent outbound calls and protect the service from saturation when a downstream dependency becomes slow or unstable.

Connection parameters

Timeouts, max connections, retries, and retry backoff are part of the communication model and can be configured per client.

Client Method Bulkhead Circuit Breaker Configured WebClient

Annotation-based configuration

@MiddlewareContract(
  name = "product",
  connection = @MiddlewareContractConnection(
    timeout = "${middleware.client.product.connection.timeout:30000}",
    maxConnections = "${middleware.client.product.connection.max-connections:50}",
    maxConcurrentCalls = "${middleware.client.product.connection.max-concurrent-calls:200}",
    maxRetries = "${middleware.client.product.connection.max-retries:3}",
    retryBackoffMillis = "${middleware.client.product.connection.retry-backoff-millis:1000}"
  ),
  circuitBreaker = @MiddlewareCircuitBreaker(
    enabled = "${middleware.client.product.circuit-breaker.enabled:true}",
    failureRateThreshold = "${middleware.client.product.circuit-breaker.failure-rate-threshold:50}",
    waitDurationInOpenStateMs = "${middleware.client.product.circuit-breaker.wait-duration-in-open-state-ms:10000}",
    slidingWindowSize = "${middleware.client.product.circuit-breaker.sliding-window-size:20}",
    minimumNumberOfCalls = "${middleware.client.product.circuit-breaker.minimum-number-of-calls:10}",
    permittedNumberOfCallsInHalfOpenState = "${middleware.client.product.circuit-breaker.permitted-number-of-calls-in-half-open-state:3}",
    statusShouldOpenBreaker = {"${middleware.client.product.circuit-breaker.status-should-open-breaker:5xx}"},
    statusShouldIgnoreBreaker = {"${middleware.client.product.circuit-breaker.status-should-ignore-breaker:4xx}"}
  )
)
public interface ProductClient {

  @GetMapping("/api/v1/products/{id}")
  ProductDto getProduct(@PathVariable("id") String id);

}

Property-based configuration

middleware:
  client:
    defaultMaxConcurrent: 25
    registry-endpoint: "http://registry.local"
    product:
      enabled: true
      connection:
        timeout: 20000
        maxConnections: 30
        maxConcurrentCalls: 80
      circuit:
        enabled: true
        failureRateThreshold: 40
        minimumNumberOfCalls: 8
        slidingWindowSize: 16
        waitDurationInOpenStateMs: 8000

Default connection values

  • Timeout: 30000 ms
  • Max connections: 50
  • Max concurrent calls: 200
  • Max retries: 3
  • Retry backoff: 1000 ms

Practical tuning guidance

  • Use conservative concurrency: especially for fragile downstream systems.
  • Avoid noisy breakers: tune minimum calls and sliding windows for low-traffic endpoints.
  • Open on server failures: treat 5xx as failures by default.
  • Ignore selected client errors: use 4xx patterns when appropriate.

Communication conventions in practice

The goal is to reduce repeated infrastructure code without hiding the runtime model. Service contracts stay explicit, remote behavior stays inspectable, and deployment concerns remain aligned with the platform.

Explicit service contracts

Remote calls are defined as interfaces with visible HTTP mappings, not hidden behind custom handwritten client plumbing.

Centralized cross-cutting behavior

Context propagation, remote errors, retries, and resilience rules are handled consistently across services.

Platform-aligned deployment model

The framework stays focused on communication semantics while infrastructure such as Kubernetes handles endpoint stability and instance distribution.