Use Case: Internal API Calls with OpenAPI Clients
About
In a microservices-based architecture, services often need to communicate with each other over HTTP. One common and robust way to do this is by generating strongly-typed clients based on shared OpenAPI specifications (also known as Swagger specs). This approach ensures that all services strictly adhere to the same contract, reduces manual boilerplate code, and makes changes more manageable through versioned specs.
This use case demonstrates a practical implementation where the payment-service
consumes APIs from account-service
using OpenAPI-generated clients.
Project Structure
Specs
account-api-spec Contains the OpenAPI definition (
account.yaml
) for retrieving account details.payment-api-spec Contains
payment.yaml
defining how to retrieve payment details.
Service
account-service Implements the account API (as per spec) using Spring Boot.
payment-service Depends on both API specs and uses OpenAPI-generated clients to call
account-service
.
1. account-api-spec
Structure
account-api-spec/
├── assembly/api.xml
├── pom.xml
└── src/main/resources/openapi/account.yaml
api.xml
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3
https://maven.apache.org/xsd/assembly-1.1.3.xsd">
<id>api</id>
<formats>
<format>zip</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>src/main/resources/openapi</directory>
<includes>
<include>account.yaml</include>
</includes>
<outputDirectory>/</outputDirectory>
</fileSet>
</fileSets>
</assembly>
account.yaml
openapi: 3.0.3
info:
title: Account API
description: Account API Specification
version: 1.0.0
servers:
- description: local test
url: http://localhost:8080/api/v1
paths:
/api/v1/accounts/{id}:
summary: Get account details by ID
description: Get account details by ID
get:
operationId: getAccountById
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Account found
content:
application/json:
schema:
$ref: '#/components/schemas/AccountResponse'
tags:
- Account
components:
schemas:
AccountResponse:
type: object
properties:
id:
type: string
name:
type: string
email:
type: string
balance:
type: number
format: double
createdAt:
type: string
format: date-time
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.company.project</groupId>
<artifactId>account-api-spec</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.7.1</version>
<executions>
<execution>
<id>assemble-api-zip</id>
<goals>
<goal>single</goal>
</goals>
<phase>package</phase>
<configuration>
<descriptors>
<descriptor>assembly/api.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Build the package
mvn clean install

2. payment-api-spec
Structure
payment-api-spec/
├── assembly/api.xml
├── pom.xml
└── src/main/resources/openapi/payment.yaml
api.xml
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3
https://maven.apache.org/xsd/assembly-1.1.3.xsd">
<id>api</id>
<formats>
<format>zip</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>src/main/resources/openapi</directory>
<includes>
<include>payment.yaml</include>
</includes>
<outputDirectory>/</outputDirectory>
</fileSet>
</fileSets>
</assembly>
payment.yaml
openapi: 3.0.3
info:
title: Payment API
description: Payment API Specification
version: 1.0.0
servers:
- description: local test
url: http://localhost:8080/api/v1
paths:
/api/v1/payments/{id}:
summary: Get payment details by ID
description: Get payment details by ID
get:
operationId: getPaymentById
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Payment found
content:
application/json:
schema:
$ref: '#/components/schemas/PaymentResponse'
tags:
- Payment
components:
schemas:
PaymentResponse:
type: object
properties:
id:
type: string
amount:
type: number
status:
type: string
account-id:
type: string
account-name:
type: string
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.company.project</groupId>
<artifactId>payment-api-spec</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.7.1</version>
<executions>
<execution>
<id>assemble-api-zip</id>
<goals>
<goal>single</goal>
</goals>
<phase>package</phase>
<configuration>
<descriptors>
<descriptor>assembly/api.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Build the package
mvn clean install

3. account-service
Structure
account-service/
├── .openapi-generator-ignore
├── pom.xml
├── src/main/java/com/company/project/controller/AccountController.java
├── src/main/java/com/company/project/AccountServiceApplication.java
└── src/main/resources/application.yaml
.openapi-generator-ignore
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# Exclude unwanted files and directories
**/src/main/AndroidManifest.xml
**/build.sbt
**/pom.xml
**/gradle/
**/git_push.sh
**/.travis.yml
**/api/openapi.yaml
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.4</version>
<relativePath />
</parent>
<groupId>com.company.project</groupId>
<artifactId>account-service</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
</dependency>
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>0.2.6</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.2.29</version>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.8.1</version>
<executions>
<execution>
<id>unpack-openapi-spec</id>
<phase>generate-sources</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>com.company.project</groupId>
<artifactId>account-api-spec</artifactId>
<version>1.0.0</version>
<classifier>api</classifier>
<type>zip</type>
<overWrite>true</overWrite>
<outputDirectory>${project.build.directory}/generated-specs/account-api-spec</outputDirectory>
</artifactItem>
</artifactItems>
<includes>**/*.yaml, **/*.json</includes>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>6.6.0</version>
<executions>
<execution>
<id>generate-spring-boot-server-code</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<generateSupportingFiles>false</generateSupportingFiles>
<generatorName>spring</generatorName>
<strictSpec>true</strictSpec>
<generateApiTests>false</generateApiTests>
<generateModelTests>false</generateModelTests>
<output>${project.build.directory}/generated-sources/openapi</output>
<inputSpec>${project.build.directory}/generated-specs/account-api-spec/account.yaml</inputSpec>
<apiPackage>com.company.project.client.account.v1.api</apiPackage>
<modelPackage>com.company.project.client.account.v1.model</modelPackage>
<configOptions>
<library>spring-boot</library>
<interfaceOnly>true</interfaceOnly>
<skipDefaultInterface>true</skipDefaultInterface>
<useTags>true</useTags>
<useBeanValidation>true</useBeanValidation>
<useClassLevelBeanValidation>false</useClassLevelBeanValidation>
<useOptional>false</useOptional>
<useJakartaEe>true</useJakartaEe>
<useSpringBoot3>true</useSpringBoot3>
<containerDefaultToNull>false</containerDefaultToNull>
</configOptions>
<ignoreFileOverride>.openapi-generator-ignore</ignoreFileOverride>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
AccountController.java
package com.company.project.controller;
import com.company.project.client.account.v1.api.AccountApi;
import com.company.project.client.account.v1.model.AccountResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class AccountController implements AccountApi {
/**
* GET /api/v1/accounts/{id}
*
* @param id (required)
* @return Account found (status code 200)
*/
@Override
public ResponseEntity<AccountResponse> getAccountById(String id) {
var accountResponse = new AccountResponse()
.id(id)
.name("Sample Account")
.balance(1000.00);
return ResponseEntity.ok(accountResponse);
}
}
AccountServiceApplication.java
package com.company.project;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AccountServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AccountServiceApplication.class, args);
}
}
application.yaml
logging:
level:
root: DEBUG
server:
port: 8082
servlet:
context-path: /
Build the package
mvn clean install

4. payment-service
Structure
payment-service/
├── .openapi-generator-ignore
├── pom.xml
├── src/main/java/com/company/project/client/AccountServiceRestClientConfig.java
├── src/main/java/com/company/project/controller/PaymentController.java
├── src/main/java/com/company/project/service/PaymentService.java
├── src/main/java/com/company/project/PaymentServiceApplication.java
└── src/main/resources/application.yaml
.openapi-generator-ignore
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# Exclude unwanted files and directories
**/src/main/AndroidManifest.xml
**/build.sbt
**/pom.xml
**/gradle/
**/git_push.sh
**/.travis.yml
**/api/openapi.yaml
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.4</version>
<relativePath />
</parent>
<groupId>com.company.project</groupId>
<artifactId>payment-service</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
</dependency>
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>0.2.6</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.2.29</version>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.8.1</version>
<executions>
<execution>
<id>unpack-openapi-spec</id>
<phase>generate-sources</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>com.company.project</groupId>
<artifactId>account-api-spec</artifactId>
<version>1.0.0</version>
<classifier>api</classifier>
<type>zip</type>
<overWrite>true</overWrite>
<outputDirectory>${project.build.directory}/generated-specs/account-api-spec</outputDirectory>
</artifactItem>
<artifactItem>
<groupId>com.company.project</groupId>
<artifactId>payment-api-spec</artifactId>
<version>1.0.0</version>
<classifier>api</classifier>
<type>zip</type>
<overWrite>true</overWrite>
<outputDirectory>${project.build.directory}/generated-specs/payment-api-spec</outputDirectory>
</artifactItem>
</artifactItems>
<includes>**/*.yaml, **/*.json</includes>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>6.6.0</version>
<executions>
<execution>
<id>generate-spring-boot-server-code</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<generateSupportingFiles>false</generateSupportingFiles>
<generatorName>spring</generatorName>
<strictSpec>true</strictSpec>
<generateApiTests>false</generateApiTests>
<generateModelTests>false</generateModelTests>
<output>${project.build.directory}/generated-sources/openapi</output>
<inputSpec>${project.build.directory}/generated-specs/payment-api-spec/payment.yaml</inputSpec>
<apiPackage>com.company.project.client.payment.v1.api</apiPackage>
<modelPackage>com.company.project.client.payment.v1.model</modelPackage>
<configOptions>
<library>spring-boot</library>
<interfaceOnly>true</interfaceOnly>
<skipDefaultInterface>true</skipDefaultInterface>
<useTags>true</useTags>
<useBeanValidation>true</useBeanValidation>
<useClassLevelBeanValidation>false</useClassLevelBeanValidation>
<useOptional>false</useOptional>
<useJakartaEe>true</useJakartaEe>
<useSpringBoot3>true</useSpringBoot3>
<containerDefaultToNull>false</containerDefaultToNull>
</configOptions>
<ignoreFileOverride>.openapi-generator-ignore</ignoreFileOverride>
</configuration>
</execution>
<execution>
<id>generate-spring-boot-client-code</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<generateSupportingFiles>true</generateSupportingFiles>
<generatorName>java</generatorName>
<strictSpec>true</strictSpec>
<generateApiTests>false</generateApiTests>
<generateApiDocumentation>false</generateApiDocumentation>
<generateModelTests>false</generateModelTests>
<generateModelDocumentation>false</generateModelDocumentation>
<output>${project.build.directory}/generated-sources/openapi</output>
<inputSpec>${project.build.directory}/generated-specs/account-api-spec/account.yaml</inputSpec>
<apiPackage>com.company.project.client.account.v1.api</apiPackage>
<modelPackage>com.company.project.client.account.v1.model</modelPackage>
<configOptions>
<library>resttemplate</library>
<dateLibrary>java8</dateLibrary>
<interfaceOnly>true</interfaceOnly>
<skipDefaultInterface>true</skipDefaultInterface>
<useTags>true</useTags>
<useBeanValidation>true</useBeanValidation>
<useClassLevelBeanValidation>false</useClassLevelBeanValidation>
<useOptional>false</useOptional>
<useJakartaEe>true</useJakartaEe>
</configOptions>
<ignoreFileOverride>.openapi-generator-ignore</ignoreFileOverride>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
AccountServiceRestClientConfig.java
package com.company.project.client;
import com.company.project.client.account.v1.ApiClient;
import com.company.project.client.account.v1.api.AccountApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AccountServiceRestClientConfig extends ApiClient {
@Value("${services.account-service.base-path}")
private String basePath;
@Bean
public AccountApi accountApi() {
return new AccountApi(createApiClient());
}
private ApiClient createApiClient() {
return new ApiClient(buildRestTemplate())
.setBasePath(basePath);
}
}
PaymentController.java
package com.company.project.controller;
import com.company.project.client.payment.v1.api.PaymentApi;
import com.company.project.client.payment.v1.model.PaymentResponse;
import com.company.project.service.PaymentService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RequiredArgsConstructor
@RestController
public class PaymentController implements PaymentApi {
private final PaymentService paymentService;
/**
* GET /api/v1/payments/{id}
*
* @param id (required)
* @return Payment found (status code 200)
*/
@Override
public ResponseEntity<PaymentResponse> getPaymentById(String id) {
log.info("Request to get payment details for id: {}", id);
return ResponseEntity.ok(paymentService.fetchPaymentDetails(id));
}
}
PaymentService.java
package com.company.project.service;
import com.company.project.client.AccountServiceRestClientConfig;
import com.company.project.client.payment.v1.model.PaymentResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class PaymentService {
private final AccountServiceRestClientConfig accountServiceRestClientConfig;
public PaymentResponse fetchPaymentDetails(String id) {
var accountDetails = accountServiceRestClientConfig.accountApi().getAccountById("DUMMY_ACCOUNT_ID");
// Return dummy details for the sake of this example
return new PaymentResponse()
.id(id)
.amount(new java.math.BigDecimal("100.0"))
.status("COMPLETED")
.accountId(accountDetails.getId())
.accountName(accountDetails.getName());
}
}
PaymentServiceApplication.java
package com.company.project;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class PaymentServiceApplication {
public static void main(String[] args) {
SpringApplication.run(PaymentServiceApplication.class, args);
}
}
application.yaml
logging:
level:
com.example: DEBUG
root: DEBUG
server:
port: 8081
servlet:
context-path: /
services:
account-service:
base-path: "http://localhost:8082/"
Build the package
mvn clean install



Verification
Start both the services using mvn spring-boot:run
mvn spring-boot:run
Test the API with Postman


Reference
Last updated