This is a simple product management application. This project contains everything you need. It includes
frontend/gocity-product-management
: React SPAbackend/gocity
: Spring Boot application which contains two sets of REST API, Product API and Category API.backend/gocity-database
: MySQL Database configuration
I use Docker to run all the components, including the frontend portal, check out How to Run for more details. After spinning up all the containers, you can access the applicaiton via:
Frontend: http://127.0.0.1:3000
Backend: http://127.0.0.1:8080/<endpoint>
Swagger UI: http://127.0.0.1:8080/swagger-ui/
- Development Environment
- Technologies Used
- Folder Structure
- How to Run
- User Interface
- System Architecture and Data Model
- Frontend Portal File Structure
- Backend Service File Structure
- Logging
- Category API
- Product API
- Future Development
I use the following tools for the development.
IDE: Visual Studio Code
OS: macOS Big Sur v11.1
- Java 11
- Spring Boot 2.5.2
- Gradle 7.1.1
- Node.js v12.14.1
- React v17.0.2
- MySQL 5.7
- Docker
- Docker Compose
The following shows the structure of this folder. There are 3 subfolders:
-
backend
(a) gocity: Backend service for this application. It contains all the source code, Dockerfile, docker-compose files, etc.
(b) gocity-database: It contains a docker-compose file of the database. -
doc: All the assets related to this document.
-
frontend
(a) gocity-product-management: React project for this application.
.
├── README.md
├── backend
│ ├── gocity
│ └── gocity-database
├── doc
│ ├── create-product.png
│ ├── gocity-architecture-diagram.png
│ ├── gocity-er-diagram.png
│ ├── landing.png
│ ├── product-details.png
│ └── product-list.png
└── frontend
└── gocity-product-management
- Docker and Docker Compose are installed on your machine.
- Port 3000, 3306 and 8080 are available on your machine.
cd backend/gocity-database
docker-compose build && docker-compose up -d
- (Stop)
docker-compose.yml up -d
cd backend/gocity
docker-compose build && docker-compose up -d
- (Stop)
docker-compose.yml up -d
cd frontend/gocity-product-management
docker-compose -f docker-compose-prod.yml build && docker-compose -f docker-compose-prod.yml up -d
- (Stop)
docker-compose-prod.yml up -d
The following shows the user interface for this application.
- path:
/
- path:
/secure/products
- path:
/secure/products/:id
- path:
/secure/products/create
The following picture shows the high level system architecture. Product management service (this service) manage the product data and product categories. Product management service exposes the Product API
and Category API
to the frontend portals. Data is stored in a MySQL database.
In our scenario, we have to types of data, category
and product
. Since each product
must have a category
, there is a one-to-many relationship between product
and category
. Because of this relationship, I decided to use a relational database, MySQL, as our data persistence layer. The following picture is the entity-relationship diagram for our database.
Each table has a id
, which is the primary key. The product
table has a foreign key (FK1 in the picture) on column category_id
, which is referencing the id
in table category
.
The fronend portal is built using React. The following shows the file structure of the application. All the React source code is stored under the src/main
folder. In most applications, authentication is required in order to gain access to the service(s). Therefore, I organize the source code as follows:
src/main/common
: Stores the common components used across the application.src/main/public
: Stores the components used in public pages, which does not require authentication, such as landing page, signup, login, etc.src/main/secure
: Stores the components used in private page, which requires authentication, such as product list, etc.src/main/util
: Stores the utility functions used across the application, such asformatDate
,httpGet
, etc.
.
├── Dockerfile
├── Dockerfile.prod
├── README.md
├── docker-compose-prod.yml
├── docker-compose.yml
├── package.json
├── public
│ ├── doge.png
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ ├── main
│ │ ├── common
│ │ ├── public
│ │ ├── secure
│ │ └── utils
│ ├── reportWebVitals.js
│ ├── setupTests.js
│ ├── static
│ │ └── doge.png
│ └── test
│ ├── common
│ ├── public
│ ├── secure
│ └── utils
└── yarn.lock
The following tree shows the file structure under the src
folder. There are 5 packages com.dennis.gocity
- Data Transfer Object (DTO): Representation of business objects (Product and Category)
- Controller: Implementation of the API endpoints
- Service: Implemenatation of the business logics
- Repository: Implemenatation of database queries. We only use Spring's default CrudRepository implementation in the project.
- Entity: JPA entities that are persisted to the database.
src
├── main
│ ├── java
│ │ └── com
│ │ └── dennis
│ │ └── gocity
│ │ ├── GocityApplication.java
│ │ ├── controller
│ │ ├── dto
│ │ ├── entity
│ │ ├── repository
│ │ └── service
│ └── resources
│ ├── application.properties
│ ├── static
│ └── templates
└── test
└── java
└── com
└── dennis
└── gocity
├── GocityApplicationTests.java
├── controller
├── dao
├── entity
├── repository
└── service
I used SLF4J for logging. The log is store under ~/datadrive/gocity/backend/log
. The path can be updated by modifying the docker-compose file inside backend/gocity
.
Returns a list of category objects in JSON format.
-
URL
/categories
-
Method:
GET
-
URL Params
None
-
Data Params
None
-
Success Response:
- Code: 200
Content:[ { "id" : 1, "categoryName" : "Example Category" } ]
- Code: 200
-
Error Response:
- Code: 500 INTERNAL SERVER ERROR
Content:null
- Code: 500 INTERNAL SERVER ERROR
Returns a category object in JSON format.
-
URL
/categories/:id
-
Method:
GET
-
URL Params
Required:
id=[integer]
-
Data Params
None
-
Success Response:
- Code: 200
Content:{ "id" : 1, "categoryName" : "Example Category" }`
- Code: 200
-
Error Response:
- Code: 500 INTERNAL SERVER ERROR
Content:null
- Code: 500 INTERNAL SERVER ERROR
Insert a category entry in the database and returns a category object in JSON format. id
is generated automatically during the insert operation.
-
URL
/categories
-
Method:
POST
-
URL Params
None
-
Data Params
{ "categoryName": "Example Category" }
-
Success Response:
- Code: 200
Content:{ "id" : 1, "categoryName" : "Example Category" }
- Code: 200
-
Error Response:
- Code: 500 INTERNAL SERVER ERROR
Content:null
- Code: 500 INTERNAL SERVER ERROR
Update a category entry in the database and returns a category object in JSON format.
-
URL
/categories/:id
-
Method:
PUT
-
URL Params
Required:
id=[integer]
-
Data Params
{ "id": 1, "categoryName": "Example Category Updated" }
-
Success Response:
- Code: 200
Content:{ "id" : 1, "categoryName" : "Example Category Updated" }
- Code: 200
-
Error Response:
- Code: 500 INTERNAL SERVER ERROR
Content:null
- Code: 500 INTERNAL SERVER ERROR
Delete a category entry in the database and returns the id
of the deleted entry as an integer.
-
URL
/categories/:id
-
Method:
DELETE
-
URL Params
Required:
id=[integer]
-
Data Params
None
-
Success Response:
- Code: 200
Content:1
- Code: 200
-
Error Response:
- Code: 500 INTERNAL SERVER ERROR
Content:null
- Code: 500 INTERNAL SERVER ERROR
Returns a list of product objects in JSON format.
-
URL
/products
-
Method:
GET
-
URL Params
None
-
Data Params
None
-
Success Response:
- Code: 200
Content:[ { "id": 1, "name": "Example Product", "description": "Example product description", "category": { "id": 1, "categoryName": "Example Category" }, "creationDate": "2021-07-13T16:41:15.388+00:00", "updateDate": "2021-07-13T16:41:15.388+00:00", "lastPurchasedDate": null } ]
- Code: 200
-
Error Response:
- Code: 500 INTERNAL SERVER ERROR
Content:null
- Code: 500 INTERNAL SERVER ERROR
Returns a product object in JSON format.
-
URL
/products/:id
-
Method:
GET
-
URL Params
Required:
id=[integer]
-
Data Params
None
-
Success Response:
- Code: 200
Content:{ "id": 1, "name": "Example Product", "description": "Example product description", "category": { "id": 1, "categoryName": "Example Category" }, "creationDate": "2021-07-13T16:41:15.388+00:00", "updateDate": "2021-07-13T16:41:15.388+00:00", "lastPurchasedDate": null }
- Code: 200
-
Error Response:
- Code: 500 INTERNAL SERVER ERROR
Content:null
- Code: 500 INTERNAL SERVER ERROR
Returns a product object in JSON format.
-
URL
/products/:id/purchase
-
Method:
POST
-
URL Params
Required:
id=[integer]
-
Data Params
{ "id": 1, "name": "Knife Set", "description": "A set of knives in all shapes and sizes.", "category": { "id": 1, "categoryName": "Kitchen" }, "creationDate": "2020-09-20T00:01:00.000+00:00", "updateDate": "2020-09-20T00:01:00.000+00:00", "lastPurchasedDate": "2020-10-24T00:01:00.000+00:00" }
-
Success Response:
- Code: 200
Content:{ "id": 1, "name": "Example Product", "description": "Example product description", "category": { "id": 1, "categoryName": "Example Category" }, "creationDate": "2021-07-13T16:41:15.388+00:00", "updateDate": "2021-07-13T16:41:15.388+00:00", "lastPurchasedDate": "2021-07-13T16:41:15.388+00:00" }
- Code: 200
-
Error Response:
- Code: 500 INTERNAL SERVER ERROR
Content:null
- Code: 500 INTERNAL SERVER ERROR
Insert a product entry in the database and returns a product object in JSON format. id
is generated automatically during the insert operation.
-
URL
/products
-
Method:
POST
-
URL Params
None
-
Data Params
{ "name": "Example Product", "description": "Example Product Description", "category": { "id": 1, "categoryName": "Example Category" } }
-
Success Response:
- Code: 200
Content:{ "id": 1, "name": "Example Product", "description": "Example Product Description", "category": { "id": 1, "categoryName": "Example Category" }, "creationDate": "2021-07-13T16:41:15.388+00:00", "updateDate": "2021-07-13T16:41:15.388+00:00", "lastPurchasedDate": null }
- Code: 200
-
Error Response:
- Code: 500 INTERNAL SERVER ERROR
Content:null
- Code: 500 INTERNAL SERVER ERROR
Update a product entry in the database and returns a product object in JSON format.
-
URL
/products/:id
-
Method:
PUT
-
URL Params
Required:
id=[integer]
-
Data Params
{ "id": 1, "name": "Example Product Updated", "description": "Example Product Description Updated", "category": { "id": 1, "categoryName": "Example Category" } }
-
Success Response:
- Code: 200
Content:{ "id": 1, "name": "Example Product Updated", "description": "Example Product Description Updated", "category": { "id": 1, "categoryName": "Example Category" }, "creationDate": "2021-07-13T16:41:15.388+00:00", "updateDate": "2021-07-13T16:41:15.388+00:00", "lastPurchasedDate": "2021-07-13T16:41:15.388+00:00" }
- Code: 200
-
Error Response:
- Code: 500 INTERNAL SERVER ERROR
Content:null
- Code: 500 INTERNAL SERVER ERROR
Delete a product entry in the database and returns the id
of the deleted entry as an integer.
-
URL
/products/:id
-
Method:
DELETE
-
URL Params
Required:
id=[integer]
-
Data Params
None
-
Success Response:
- Code: 200
Content:1
- Code: 200
-
Error Response:
- Code: 500 INTERNAL SERVER ERROR
Content:null
- Code: 500 INTERNAL SERVER ERROR
- (Backend) Implement authentication layer using
Spring Security
- (UI Improvement) Transform the existing landing page into a login page
- (UI Improvement) Support category management
- (UI Improvement) Add navigation bar and side bar
- ...