A Spring Boot REST API for managing a product catalog, built with a dual-datasource strategy — a live MySQL database and an external FakeStore API proxy.
The project also includes a hands-on demonstration of all four JPA inheritance mapping strategies.
| Layer | Technology |
|---|---|
| Language | Java 17 |
| Framework | Spring Boot 3.2.3 |
| Persistence | Spring Data JPA + Hibernate |
| Database | MySQL 8 |
| External API | FakeStore API via RestTemplate |
| Boilerplate Reduction | Lombok |
| Build Tool | Maven |
The service exposes a single REST controller (ProductController) backed by a swappable service layer via Spring's @Qualifier annotation.
Client Request
│
▼
ProductController (/products)
│
├─── @Qualifier("SelfProductService")
│ └── ProductServiceImpl ──► ProductRepo (MySQL JPA)
│ └── CategoryRepo (MySQL JPA)
│
└─── @Qualifier("FakeProductService")
└── FakeStoreProductServiceImpl ──► FakeStoreClient (RestTemplate)
└── fakestoreapi.com
Switch between the two implementations by changing the @Qualifier in ProductController.
No other code needs to change — this is the Strategy Pattern in action.
Base path: /products
| Method | Endpoint | Description |
|---|---|---|
| GET | /products/{id} |
Get a product by ID |
| GET | /products |
Get all products |
| POST | /products |
Create a new product |
| PUT | /products/{id} |
Update an existing product |
| DELETE | /products/{id} |
Delete a product by ID |
POST /products
{
"title": "Wireless Headphones",
"description": "Over-ear noise cancelling",
"price": 2999,
"category": {
"name": "Electronics"
}
}HTTP 404
{
"message": "Product not found with id: 42",
"status": "failure"
}src/main/java/com/example/productservicecp/
│
├── ProductServiceJanBatchApplication.java
│
├── controllers/
│ ├── ProductController.java
│ └── advices/
│ └── ProductControllerAdvice.java
│
├── services/
│ ├── ProductService.java
│ ├── ProductServiceImpl.java
│ ├── FakeStoreProductServiceImpl.java
│ └── FakeproductServiceObjectMapper.java
│
├── thirdpartyclients/
│ └── FakeStoreClient.java
│
├── models/
│ ├── BaseModel.java
│ ├── Product.java
│ └── Category.java
│
├── repository/
│ ├── ProductRepo.java
│ └── CategoryRepo.java
│
├── dtos/
│ ├── FakeStoreProductDto.java
│ └── ExceptionDto.java
│
├── exceptions/
│ └── ProductNotFoundException.java
│
└── inheritancedemo/
├── singletable/
├── tableperclass/
├── joinedtable/
└── mappedsuper/
| Field | Type | Notes |
|---|---|---|
| id | Long | Auto-generated primary key |
| title | String | Product name |
| description | String | Product description |
| price | Long | Price in smallest currency unit |
| category | Category | @ManyToOne relationship |
| Field | Type | Notes |
|---|---|---|
| id | Long | Auto-generated |
| name | String | Category name |
When adding or updating a product, the service checks whether the category already exists by name using:
CategoryRepo.findByName()If it exists, the existing category is reused. Otherwise, a new category is created.
This prevents duplicate category rows.
ProductService interface with two implementations:
ProductServiceImplFakeStoreProductServiceImpl
The controller depends on the interface, not the concrete class.
Implementation is swapped via @Qualifier.
FakeStoreProductDto decouples the internal Product domain model from the external API contract.
Conversion happens inside FakeStoreProductServiceImpl.
ProductControllerAdvice uses @RestControllerAdvice scoped to ProductController.
It catches ProductNotFoundException and returns structured JSON error responses.
FakeStoreClient encapsulates all RestTemplate logic for external API interaction.
The service layer never directly interacts with RestTemplate.
The inheritancedemo package demonstrates all four Hibernate inheritance strategies using a User → Mentor / Student hierarchy.
| Strategy | Tables Created | Trade-off |
|---|---|---|
| SINGLE_TABLE | One table with discriminator column | Fast reads, nullable columns |
| TABLE_PER_CLASS | One table per concrete class | No joins, duplicated schema |
| JOINED | Parent + child tables | Normalized, slower joins |
| MAPPED_SUPERCLASS | Only child tables | No polymorphic parent queries |
- Java 17+
- Maven 3.8+
- MySQL 8
CREATE DATABASE jan24productservice;
CREATE USER 'jan24productservice'@'localhost'
IDENTIFIED BY 'your_password';
GRANT ALL PRIVILEGES
ON jan24productservice.*
TO 'jan24productservice'@'localhost';Update:
src/main/resources/application.propertiesspring.datasource.password=your_password./mvnw spring-boot:runApplication starts on:
http://localhost:8080
In ProductController.java:
// Use local MySQL
@Qualifier("SelfProductService")
// Use FakeStore API
@Qualifier("FakeProductService")# FakeStore API
fakestore.api.url=https://fakestoreapi.com/products/{id}
# Database
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/jan24productservice
spring.datasource.username=jan24productservice
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# Hibernate
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect- Elasticsearch
- Kafka
- Spring Cloud / Microservices
- Authentication / Authorization
- Pagination on
GET /products - Full implementation of
getAllProducts() - Full implementation of
deleteProductById()
MIT