Skip to content

This repository implements an API, based on Java and Spring Boot, that reads data from a database.

License

Notifications You must be signed in to change notification settings

SaulEiros/price-explorer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Issues MIT License Live Demo LinkedIn

Table of Contents
  1. About The Project
  2. Project Implementation
  3. Usage
  4. Further Work
  5. Contribution

About The Project

This project is a technical demonstration of how to build a simple project using Java 21 and Spring Boot. This project also wants to reflect clean code and clean architecture techniques and best practices.

The purpose of the application is to list the assigned prices in a database table. The query is filtering the results using the price application date, brand or product ID; all of them mandatory. If two or more prices matches for the same application date, the one with the highest priority will be returned.

(back to top)

Built With

The application was created using Spring Initializr based on Spring Boot 3.3.3 and * java 21*.

These are the tools with which the application has been built:

  • SpringBoot
  • Java
  • Junit
  • Mockito
  • Swagger

(back to top)

How To Explore The Repository

The repository was tagged each time a significant change was made so that the entire development process could be explored.

These are the different tags available:

  • base-project: The project created with Spring Initializr
  • base-structure: Added domain objects, separated the application into layers and added service interfaces.
  • price-service-impl: Added Mockito dependency, added Price Service Unit Tests and Implementation.
  • output-adapters-impl: Added H2 Integration and implementation the Sql Price Repository.
  • input-adapters-impl: Added Swagger dependencies and Rest Controllers implementation.
  • dockerized-app: Added Docker and Docker Compose files. Added Live Demo and Enhanced README.md.
  • docker-image-publish: Added github actions workflow for build and publish imagen in Dockerhub registry.
  • refactor-business-logic: A less complex approach was requested. This ended up with slight modifications to the application.
  • e2e-testing: To validate the entire application, a collection of e2e tests was implemented.

(back to top)

Project Implementation

This section explores how different aspects of development were addressed for the reader's clarity.

(back to top)

Git Workflow

All the developments were done in a feature branch and merged to main once the branch scope was complete. Once the changes reach main, a new tag is created so that they can be easily located.

Also, Conventional Commits specification (see more) was followed.

Perhaps in a real scenario, merging the changes by squashing commits would have been appropriate, but in this case I have chosen to merge all commits so that the entire development process could be seen.

(back to top)

Hexagonal Architecture

Hexagonal Architecture, also known as Ports and Adapters Architecture (read more), is an architectural pattern that aims to create applications with low coupling between its different components.

This allows its components to be easily replaced without compromising other parts of the software. In addition, it also facilitates the maintenance and extension of the code, as well as its testing.

It may seem that for such a simple application it is not necessary. But if, in the future, we would like to add more functionality, integrate with a database to create a real repository of images or add extra functionality, having developed the application following this pattern will make the process much easier.

The main code is separated into the following folder hierarchy:

main
├── PriceexplorerApplication.kt
├── application
    ...
├── domain
    ...
└── infrastructure
    ...
  • application: This layer contains the implementation of the application's business logic. It also defines interfaces of input adapters (services that implement the business logic) and output adapters (connections to external repositories and services).
  • domain: This layer implements the objects that define the core elements of the business.
  • infrastructure: This layer implements the input and output adapters (The input and output connections with external services.)

(back to top)

Testing

Unit Testing

Unit tests for the application layer were developed using Junit 5 and Mockito. Also, TTD (see more) was followed, creating first the test suit and implementing the services later.

To track this during development, you can consult the commit history for the tags price-service-impl.

Additionally, to structure the tests, I have tried to follow the GIVEN/THEN/WHEN pattern (see more).

Here is an example of such tests:

@Test
    void givenOneExistingPrice_whenSearchingForMatchingProperties_thenPriceIsReturned() {
        // GIVEN
        Random random = new Random();
        LocalDateTime startDate = LocalDateTime.of(2024, 1, 1, 0, 0);
        LocalDateTime endDate = LocalDateTime.of(2024, 12, 31, 23, 59);
        Long brandId = random.nextLong();
        Long productId = random.nextLong();
        Long priority = random.nextLong();
        Double price = random.nextDouble();

        LocalDateTime searchedDate = LocalDateTime.of(2024, 7, 2, 12, 0);

        Price expectedPrice = generatePrice(startDate, endDate, brandId, productId, priority, price);

        when(priceRepository.findPrices(searchedDate, brandId, productId)).thenReturn(List.of(expectedPrice));

        // WHEN
        Price result = priceService.findPrice(searchedDate, brandId, productId);

        // THEN
        verify(priceRepository, times(1)).findPrices(searchedDate, brandId, productId);
        assertNotNull(result);
        assertEquals(expectedPrice, result);
    }

E2E Testing

Additionally, a collection of E2E tests was implemented for validating the whole implementation. The test collection validates the following cases:

  • 14th of June at 10:00 for brand 1 and product 35455:
http://localhost:8080/prices?date=2020-06-14T10:00:00&brandId=1&productId=35455
Response
{
  "productId": 35455,
  "brandId": 1,
  "priceList": 1,
  "price": 35.5,
  "currency": "EUR",
  "startDate": "2020-06-14T00:00:00",
  "endDate": "2020-12-31T23:59:59"
}
  • 14th of June at 16:00 for brand 1 and product 35455:
http://localhost:8080/prices?date=2020-06-14T16:00:00&brandId=1&productId=35455
Response
{
  "productId": 35455,
  "brandId": 1,
  "priceList": 2,
  "price": 25.45,
  "currency": "EUR",
  "startDate": "2020-06-14T15:00:00",
  "endDate": "2020-06-14T18:30:00"
}
  • 14th of June at 21:00 for brand 1 and product 35455:
http://localhost:8080/prices?date=2020-06-14T21:00:00&brandId=1&productId=35455
Response
{
  "productId": 35455,
  "brandId": 1,
  "priceList": 1,
  "price": 35.5,
  "currency": "EUR",
  "startDate": "2020-06-14T00:00:00",
  "endDate": "2020-12-31T23:59:59"
}
  • 15th of June at 10:00 for brand 1 and product 35455:
http://localhost:8080/prices?date=2020-06-15T10:00:00&brandId=1&productId=35455
Response
{
  "productId": 35455,
  "brandId": 1,
  "priceList": 3,
  "price": 30.5,
  "currency": "EUR",
  "startDate": "2020-06-15T00:00:00",
  "endDate": "2020-06-15T11:00:00"
}
  • 16th of June at 21:00 for brand 1 and product 35455:
http://localhost:8080/prices?date=2020-06-16T21:00:00&brandId=1&productId=35455
Response
{
  "productId": 35455,
  "brandId": 1,
  "priceList": 4,
  "price": 38.95,
  "currency": "EUR",
  "startDate": "2020-06-15T16:00:00",
  "endDate": "2020-12-31T23:59:59"
}
  • 404 ERROR CODE: 16th of June at 21:00 for brand 2 and product 35455:
http://localhost:8080/prices?date=2020-06-16T21:00:00&brandId=2&productId=35455
Response
{
  "status": 404,
  "message": "Price not found for product 35455, brand 2 on date 2020-06-16T21:00"
}
  • 400 BAD REQUEST (Missing Parameter): 16th of June at 21:00 for brand 1:
http://localhost:8080/prices?date=2020-06-16T21:00:00&brandId=1
Response
{
  "status": 400,
  "message": "The productId parameter is missing"
}
  • 400 BAD REQUEST (Invalid Parameter): 16th of June at 21:00 for brand 1 and product 1Z:
http://localhost:8080/prices?date=2020-06-16T21:00:00&brandId=1&productId=1Z
Response
{
  "status": 400,
  "message": "The productId parameter is not of the correct type: Long"
}

(back to top)

Project Configuration

There are a few properties configured for the smooth running of the project:

spring.application.name=priceexplorer
# H2 Configuration
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.sql.init.platform=h2
spring.jpa.defer-datasource-initialization=true
# H2 Console
spring.h2.console.enabled=false
spring.h2.console.path=/h2-console
spring.jpa.show-sql=true

It basically contains the configuration of the H2 In Memory database. Special attention to the property spring.jpa.defer-datasource-initialization=true that will allow us to execute the data.sql file (located in src/main/resources) after the creation of the schemas by Hibernate.

This is the content of data.sql to initialize our data when the application is loaded.

INSERT INTO prices (brand_id, start_date, end_date, price_list, product_id, priority, price, currency, last_update,
                    last_update_by)
VALUES (1, '2020-06-14 00:00:00', '2020-12-31 23:59:59', 1, 35455, 0, 35.50, 'EUR', '2020-03-26 14:49:07', 'user1'),
       (1, '2020-06-14 15:00:00', '2020-06-14 18:30:00', 2, 35455, 1, 25.45, 'EUR', '2020-05-26 15:38:22', 'user1'),
       (1, '2020-06-15 00:00:00', '2020-06-15 11:00:00', 3, 35455, 1, 30.50, 'EUR', '2020-05-26 15:39:22', 'user2'),
       (1, '2020-06-15 16:00:00', '2020-12-31 23:59:59', 4, 35455, 1, 38.95, 'EUR', '2020-06-02 10:14:00', 'user1');

(back to top)

Usage

Prerequisites

If you want to run the application, you must make sure that you have the following dependencies installed:

(back to top)

Run The Application

To run the application it is necessary to type the following commands:

gradle build
gradle bootRun

If you only want to run the test, run the following command:

gradle test

(back to top)

Use The Application

To use the application you can access the Swagger UI. To do so, you can open a browser and go to this url:

http://localhost:8080/swagger-ui/index.html

You can also perform requests to the API invoking directly the developed endpoint like in the following example:

http://price-explorer.sauleiros.com/prices?date=2020-06-16T21:00:00&brandId=1&productId=35455
Response
{
  "productId": 35455,
  "brandId": 1,
  "priceList": 4,
  "price": 38.95,
  "currency": "EUR",
  "startDate": "2020-06-15T16:00:00",
  "endDate": "2020-12-31T23:59:59"
}

Remember that all the parameters are mandatory.

Also, the Application can return other responses:

  • 400: If there is any missing parameter or any parameter does not match the required type.
  • 404: If there is no prices that matches the requested parameters.
  • 500: If there is any internal error during the execution of the request.

(back to top)

Docker Image

To facilitate the deployment of the application, if you have Docker on your system you can run the application with the following command:

docker-compose up -d

Once the container is up and running, you can access it as normal:

On the swagger front end:

http://localhost:8080/swagger-ui/index.html

By querying the api directly, as in this example:

http://price-explorer.sauleiros.com/prices?date=2020-06-16T21:00:00&brandId=1&productId=35455

Aditionaly, a Github Actions Workflow was created for build and publish the image every time a new change is pushed into main (see workflow).

You do not need to download the project for executing it in your local. In order to use the published image, just create a docker-compose.yml file like this:

services:
  backend:
    container_name: price-explorer-backend
    image: mreiros/price-explorer-backend:latest
    volumes:
      - ./src:/app/src
    ports:
      - 8080:8080

Once you have the file created, just run the following command:

docker-compose up -d

(back to top)

LIVE DEMO

A live demo is also available without downloading the code. You can consult the swagger panel at the following link:

http://price-explorer.sauleiros.com/swagger-ui/index.html

Or make requests directly to the api as in this example:

http://price-explorer.sauleiros.com/prices?date=2020-06-16T21:00:00&brandId=1&productId=35455

Note that HTTPS requests are not available. Enabling SSL connections in Swagger is a work in progress.

(back to top)

FURTHER WORK

Activate SSL on Swagger Connections:

There is currently a problem accessing the Swagger panel in the Live Demo. If accessed via https, it will not be possible to test operations.

Integrating a CI/CD flow in github:

It would be great to be able to integrate the changes automatically into the Demo server. For this, a CI/CD plan could be created using the pipelines offered by github.

Improving exception handling at the REST layer:

Currently, the exception handling done at the REST layer is limited and could be improved by providing for a larger volume of exceptions.

Adding a complete CRUD repository:

Currently, search for prices is the only available operation. It would be interesting to create operations for create, update and delete prices.

(back to top)

Contribution

The project has been developed for training purposes, so if you have any suggestions, you are more than welcome to leave a comment by opening an Issue in the repository or contacting me through my LinkedIn.

(back to top)

About

This repository implements an API, based on Java and Spring Boot, that reads data from a database.

Resources

License

Stars

Watchers

Forks

Packages

No packages published