Use Case: Internal API Calls with OpenAPI Clients

About

In a typical microservices architecture, services frequently need to communicate with each other to perform operations or retrieve data. This use case demonstrates how OpenAPI-generated clients can be used to make internal service-to-service calls in a Spring Boot-based system, ensuring:

  • Strong typing and IDE support

  • Reduced boilerplate

  • Consistent request and response handling

  • Alignment with the contract defined in OpenAPI specs

This setup uses two services:

  • account-service: Exposes an endpoint to retrieve account details.

  • payment-service: Calls the account-service using a client generated from account-api-spec.

Both services are aligned on their contracts using shared OpenAPI specifications and generate client code during build time using the OpenAPI Generator with the spring-webclient library.

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.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</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>webclient</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(buildWebClient())
            .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")
            .block();

        // 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

Test the API with Postman

Reference

Last updated