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:
- There is no main application class.
- The
build.gradlefile is slightly different because we are not building a fat JAR. - We use the Spring Boot platform for dependency management instead of applying the Spring Boot Gradle plugin.
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
) {
}
- Using a record makes the data immutable.
- Records automatically generate constructor, getters, equals, hashCode, and toString.
- Ideal for API models shared across microservices.
ChapterService Interface
public interface ChapterService {
@GetMapping
List<Chapter> getChaptersByCourseId(@RequestParam(value = "courseId") Long courseId);
}
- Base path:
/api/v1/chaptersensures versioning and clarity. - CRUD operations fully annotated:
@GetMapping,@PostMapping,@PutMapping,@DeleteMapping. - Path variables and request bodies annotated properly.
- Ready to be implemented in the Chapter microservice.
Course Record
public record Course(
Long courseId,
String title,
String description,
String serviceAddress,
CourseDifficultyLevel difficultyLevel
) {
}
- Immutable, clean, and perfect for an API data model.
- Auto-generates constructor, getters, equals, hashCode, and toString
CourseService Interface
public interface CourseService {
@GetMapping("/{courseId}")
Course getCourseById(@PathVariable Long courseId);
}
- Base path includes API version:
/api/v1/courses. - Methods use Spring REST annotations for full CRUD:
@GetMapping,@PostMapping,@PutMapping,@DeleteMapping. - Fully consistent with ChapterService style.
- Ready to be implemented in the Course microservice.
Quiz Record
public record Quiz(
Long quizId,
Long chapterId,
String question,
String correctAnswer,
String serviceAddress,
List<String> answerOptions
) {
}
- Immutable data model, perfect for API usage.
- Automatically generates constructor, getters, equals, hashCode, and toString.
CourseService Interface
public interface QuizService {
@GetMapping
List<Quiz> getQuizzesByChapterId(@RequestParam(value = "chapterId") Long chapterId);
}
- Base path includes API version:
/api/v1/quizzes. - Fully annotated CRUD operations:
@GetMapping,@PostMapping,@PutMapping,@DeleteMapping. - Follows the same consistent style as ChapterService and CourseService.
- Ready to be implemented in the Quiz microservice.
Creating Custom Exceptions
When we build microservices, errors and exceptions are inevitable. Handling them properly is very important because it helps us:
- Give clear messages to clients when something goes wrong.
- Keep our code clean and consistent.
- Avoid repeating the same error-handling logic in multiple places.
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:
- Group all exceptions in one place for easier management.
- Ensure all exceptions share common features, like a message or a cause.
- Make it easier to handle exceptions globally, for example in a central error handler.
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);
}
}
- It extends
RuntimeException, so we don’t have to declare it in every method signature. - All other custom exceptions will inherit from it.
We need two main exceptions for our microservices:
InvalidRequestException– for invalid input or bad requests.NotFoundException– for cases when a resource is not found.
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
- Consistency: All exceptions are grouped and handled in the same way.
- Clean code: No repeated error messages or logic scattered across the project.
- Easy error handling: We can write a global exception handler to map exceptions to proper HTTP responses (e.g., 400, 404).
- Reusability: All microservices can use the same API module exceptions.
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'
include ':api'tells Gradle to include the API project in the multi-project build.- All other services remain included as before.
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()
}
implementation project(':api')imports the API module into the microservice.- You can now use
Chapter,ChapterService, and other API classes directly in your service implementation. - This approach ensures all microservices share the same API definitions, avoiding duplication.
- Works the same for
course-service,quiz-service, andcourse-composite-service—just replace the groupId and service-specific dependencies.
The source code for this article is available over on GitHub.