Creating the API Library

The API project will be packaged as a library, not a standalone application. This means it won’t have a main class like a typical Spring Boot application. Instead, it only contains the code that other microservices can use, such as API interfaces, data models, and exceptions.

Unfortunately, Spring Initializr doesn’t provide an option to create a library project directly. So we need to create it manually from scratch.

Structure of the API library

The structure is very similar to a normal Spring Boot application project, with a few differences:

Here is an example build.gradle for the API library:

plugins {
	id 'java'
	id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.htp.microservices.api'
version = '1.0.0-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
	mavenCentral()
}

ext {
    springBootVersion = '3.5.7'
    lombokVersion = '1.18.42'
}

dependencies {
    implementation platform("org.springframework.boot:spring-boot-dependencies:${springBootVersion}")

    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    compileOnly "org.projectlombok:lombok:${lombokVersion}"
    annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

Here is the project structure:

/microservices-with-spring-boot/api/src/main/java/com/htp/microservices/api
├── core
│   ├── chapter
│   │   ├── Chapter.java                # Data model representing a chapter
│   │   └── ChapterService.java         # Interface defining the Chapter REST API
│   │
│   ├── course
│   │   ├── Course.java                 # Data model representing a course
│   │   └── CourseService.java          # Interface defining the Course REST API
│   │
│   └── quiz
│       ├── Quiz.java                   # Data model representing a quiz
│       └── QuizService.java            # Interface defining the Quiz REST API
│
└── exceptions
    ├── BaseException.java              # Common base class for custom exceptions
    ├── InvalidRequestException.java    # Thrown for invalid API requests
    └── NotFoundException.java          # Thrown when a resource is not found

Chapter Record

public record Chapter(
        Long chapterId,
        Long courseId,
        String title,
        String content,
        String serviceAddress
) {
}

ChapterService Interface

public interface ChapterService {

    @GetMapping
    List<Chapter> getChaptersByCourseId(@RequestParam(value = "courseId") Long courseId);
}

Course Record

public record Course(
        Long courseId,
        String title,
        String description,
        String serviceAddress,
        CourseDifficultyLevel difficultyLevel
) {
}

CourseService Interface

public interface CourseService {

    @GetMapping("/{courseId}")
    Course getCourseById(@PathVariable Long courseId);
}

Quiz Record

public record Quiz(
        Long quizId,
        Long chapterId,
        String question,
        String correctAnswer,
        String serviceAddress,
        List<String> answerOptions
) {
}

CourseService Interface

public interface QuizService {

    @GetMapping
    List<Quiz> getQuizzesByChapterId(@RequestParam(value = "chapterId") Long chapterId);
}

Creating Custom Exceptions

When we build microservices, errors and exceptions are inevitable. Handling them properly is very important because it helps us:

In this section, we will create a set of custom exceptions that all microservices can use.

Why do we need a BaseException?

A BaseException acts as the parent for all custom exceptions. By using a base class, we can:

Here’s the BaseException class:

public class BaseException extends RuntimeException {

    public BaseException() {
        super();
    }

    public BaseException(String message) {
        super(message);
    }

    public BaseException(String message, Throwable cause) {
        super(message, cause);
    }

    public BaseException(Throwable cause) {
        super(cause);
    }

    public BaseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

We need two main exceptions for our microservices:

InvalidRequestException:

public class InvalidRequestException extends BaseException {

    public InvalidRequestException(String message) {
        super(message);
    }

    public InvalidRequestException(String message, Throwable cause) {
        super(message, cause);
    }
}

NotFoundException:

public class NotFoundException extends BaseException {

    public NotFoundException(String message) {
        super(message);
    }

    public NotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}

Benefits of this approach

Using the API Module as a Library

Now that we have created a separate API module, we want our microservices to reuse the same data models and service interfaces. By including the API module in the Gradle build and adding it as a dependency, we avoid duplicating code and ensure consistency across services.

Include the API Module in settings.gradle

In your multi-project build, add the API module like this:

rootProject.name = 'microservices-with-spring-boot'

include ':api'
include ':microservices:chapter-service'
include ':microservices:course-service'
include ':microservices:course-composite-service'
include ':microservices:quiz-service'

Add the API Module as a Dependency

Each microservice can now depend on the API module. This allows the service to use the records and interfaces defined in the API project.

Example: chapter-service/build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.5.7'
	id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.htp.microservices.core.chapter'
version = '1.0.0-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
	mavenCentral()
}

dependencies {
    implementation project(':api')

	implementation 'org.springframework.boot:spring-boot-starter-actuator'
	implementation 'org.springframework.boot:spring-boot-starter-webflux'
    compileOnly "org.projectlombok:lombok"
    annotationProcessor "org.projectlombok:lombok"
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'io.projectreactor:reactor-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
	useJUnitPlatform()
}

The source code for this article is available over on GitHub.