How the HTTP communication between Spring microservices in the Whoz back-end is implemented

Thomas Martin
WhozApp
Published in
3 min readFeb 16, 2024

A practical story about defining interfaces, clients, and controllers

Generated with AI ∙ 13 February 2024 at 9:53 am

Coupling and compatibility issues

Whoz backend is based on a dozen Spring Boot microservices that expose endpoints and call each other.

Several years ago, we had a module called api-client in our monorepo. We put in there Feign clients and the data transfer objects (DTOs) used for exchanging information.

This became a massive module that all microservices depended on, which led to three main issues:

  • Small changes in a service, like adding a property in an endpoint’s response, were triggering the build for all microservices.
  • A breaking change in an endpoint contract could lead to runtime errors if api-client ‘s Feign clients were not updated as well
  • Business and architecture rules could be easily violated because each microservice could call any other. Indeed, there is a logic where a service A can call a service B, but not the other way around.

The Three-Tier Structure

To solve those issues, we have split our back-end services into three distinct components: xxx-service, xxx-api, and xxx-client.

This separation of concerns ensures modularity, compatibility, and maintainability of our services.

Let’s take an example with our “casting” service.

Casting-Service

The casting-service is the core microservice that implements the actual business logic, handles data persistence, and exposes RESTful endpoints. It is a Spring Boot application that uses controllers to define endpoints and services for business logic.

Casting-API

The casting-api module defines the interfaces for the service endpoints and the data transfer objects (DTOs) used for exchanging information. This module acts as a contract between the service and its clients, ensuring that both sides agree on the communication protocol.

Casting-Client

The casting-client is a library that other services use to communicate with the casting-service. It includes Feign clients that are interfaces mirroring the casting-api endpoints.

Advantages

By implementing the same interface in both the controller within the casting-service and the Feign client in the casting-client, we ensure compatibility and ease of communication.

Services are not tightly bound to each other, allowing for independent development and deployment. Also, each microservice must include the api and client dependency, here casting-api and casting-client . So in code review, an architecture mistake is easily spotted because of the wrongly added dependency.

Code snippets

casting-api

package casting.api.project

interface ProjectMatchApi {
fun autocomplete(@Valid projectAutocompleteInput: ProjectAutocompleteInput): List<ProjectAutocompleteOutput>
}
package casting.api.project.resource.input

data class ProjectAutocompleteInput(
@field:NotBlank
val federationId: String,

@field:NotBlank
val input: String,
)
package casting.api.project.resource.output

data class ProjectAutocompleteOutput (
val federationId: String,
val projectId: String,
val score: Float,
)

casting-service

package casting.api.project

import casting.api.project.resource.input.ProjectAutocompleteInput
import casting.api.project.resource.output.ProjectAutocompleteOutput
import casting.service.project.ProjectMatcher

@Validated
@RestController
@RequestMapping(path = ["/api/casting/project"])
class ProjectMatchController(
private val projectMatcher: ProjectMatcher,
) : ProjectMatchApi{

@PostMapping("/autocomplete")
override fun autocomplete(@RequestBody @Valid projectAutocompleteInput: ProjectAutocompleteInput): List<ProjectAutocompleteOutput> =
projectMatcher.autocomplete(projectAutocompleteInput)
}

casting-client

package io.biznet.casting.client.project

import casting.api.project.ProjectMatchApi
import casting.api.project.resource.input.ProjectAutocompleteInput
import casting.api.project.resource.output.ProjectAutocompleteOutput


@FeignClient(name = "casting", contextId = "castingProjectMatchApiClient")
interface ProjectMatchApiClient : ProjectMatchApi {
@PostMapping("/api/casting/project/autocomplete")
override fun autocomplete(
@RequestBody projectAutocompleteInput: ProjectAutocompleteInput
): List<ProjectAutocompleteOutput>
}

Note the contextId to avoid any clash with other services’ endpoints. casting-client also defines a Spring configuration to be imported by other services:

package casting.client.config

@Configuration
@EnableFeignClients("casting.client")
open class CastingFeignClientsConfig

Another service that needs calling casting

A service needing to communicate with casting-service includes casting-client and casting-api as a dependency. Also, it needs to import the configuration:

@Import([
CastingFeignClientsConfig
])
@SpringBootApplication
class SomeOtherApplication {}

The consuming service autowires the Feign client interface provided by casting-client.

The request is serialized into the ProjectAutocompleteInput DTO defined in casting-api. The response is deserialized into the ProjectAutocompleteOutputDTO defined in casting-api and returned to the consuming service.

At Whoz, the HTTP communication between Spring microservices is streamlined through a well-structured approach using service, api, and client modules. This structure promotes clean separation of concerns, ensures compatibility, and simplifies the process of inter-service communication.

How does our approach to structuring HTTP communication in Spring microservices compare to your experiences? Are there alternative patterns or tools you’ve found effective for microservice HTTP interaction?

--

--