🔥 A Go-based implementation is available in the campsite-booking-go repository.
Read these articles to get more insights:
Table of Contents
- The campsite can be reserved for max 3 days.
- The campsite can be reserved minimum 1 day(s) ahead of arrival and up to 1 month in advance.
- Reservations can be cancelled anytime.
- For sake of simplicity assume the check-in & check-out time is 12:00 AM.
- The users will need to find out when the campsite is available. So the system should expose an API to provide information of the availability of the campsite for a given date range with the default being 1 month.
- Provide an end point for reserving the campsite. The user will provide his/her email & full name at the time of reserving the campsite along with intended arrival date and departure date. Return a unique booking identifier back to the caller if the reservation is successful.
- The unique booking identifier can be used to modify or cancel the reservation later on. Provide appropriate end point(s) to allow modification/cancellation of an existing reservation.
- Due to the popularity of the campsite, there is a high likelihood of multiple users attempting to reserve the campsite for the same/overlapping date(s). Demonstrate with appropriate test cases that the system can gracefully handle concurrent requests to reserve the campsite.
- Provide appropriate error messages to the caller to indicate the error cases.
- The system should be able to handle large volume of requests for getting the campsite availability.
- There are no restrictions on how reservations are stored as long as system constraints are not violated.
This project is implemented using an API-first(or contract-first) approach along with Maven's multi-module project structure. You can read more here on the API-first development approach.
$ git clone https://github.com/igor-baiborodine/campsite-booking.git
$ cd campsite-booking
$ mvn clean install -DskipTests -DskipITs
$ mvn spring-boot:run -Dspring-boot.run.profiles=in-memory-db -f campsite-booking-service/pom.xml
The Swagger UI is available at http://localhost:8080/swagger-ui.html
.
$ git clone https://github.com/igor-baiborodine/campsite-booking.git
$ cd campsite-booking
$ mvn clean install -DskipTests -DskipITs
$ mvn package spring-boot:repackage -DskipTests -DskipITs -f campsite-booking-service/pom.xml
$ java -jar -Dspring.profiles.active=in-memory-db campsite-booking-service/target/campsite-booking-service-<version>.jar
The Swagger UI is available at http://localhost:8080/swagger-ui.html
.
$ git clone https://github.com/igor-baiborodine/campsite-booking.git
$ cd campsite-booking
$ docker build --rm --file container/Dockerfile --tag campsite-booking-service .
$ docker run -e "SPRING_PROFILES_ACTIVE=in-memory-db" --name campsite-booking-service -d campsite-booking-service
$ docker logs -f campsite-booking-service
The Swagger UI is available at http://<container-ip>:8080/swagger-ui.html
. To get the container IP
address, execute the following command:
$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' campsite-booking-service
Via the host machine on port 80:
$ docker run -e "SPRING_PROFILES_ACTIVE=in-memory-db" --name campsite-booking-service -p 80:8080 -d campsite-booking-service
The Swagger UI is available at http://localhost:80/swagger-ui.html
or http://host-ip:80/swagger-ui.html
.
... or with an image from Docker Hub:
$ docker run -e "SPRING_PROFILES_ACTIVE=in-memory-db" --name campsite-booking-service -p 80:8080 -d ibaiborodine/campsite-booking-service
... or with in-memory DB docker-compose:
$ docker compose -f container/docker-compose.yml up -d
... or with MySQL docker-compose:
$ docker compose -f container/campsite-booking-service-mysql/docker-compose.yml up -d
- Run only unit tests:
$ mvn clean test
- Run only integration tests:
$ mvn clean failsafe:integration-test
- Run unit and integration tests:
$ mvn clean verify
- Run SonarCloud analysis, including test coverage, code smells, vulnerabilities, etc.:
$ mvn clean verify sonar:sonar -Dsonar.login=<SONAR_TOKEN>
The API can be tested via the Swagger UI:
For example, to add a new booking, expand the POST
operation. Then click on the Try it out
, add
the payload below to the Request Body
text area, and click on the Execute
:
{
"campsiteId": 1,
"email": "John Smith",
"fullName": "[email protected]",
"startDate": "2023-11-19",
"endDate": "2023-11-21",
"active": true
}
If the operation is successful, you will get the following response:
Start an instance of the Campsite Booking API via Docker Compose either in the in-memory-db or in mysql profile.
$ docker-compose.yml up -d
Execute the concurrent-create-bookings.sh script to simulate concurrent booking creation for the same booking dates:
$ ./script/test/concurrent-create-bookings.sh 2023-11-16 2023-11-17 http:/localhost:80
The response should be as follows after formatting, i.e., only one booking was created:
[
{
"uuid": "4da1818c-2d9e-4efe-b59d-38915d6bc5d3",
"version": 0,
"campsiteId": 1,
"email": "[email protected]",
"fullName": "John Smith 2",
"startDate": "2023-11-16",
"endDate": "2023-11-17",
"active": true
},
{
"status": 400,
"message": "No vacant dates available from 2023-11-16 to 2023-11-17",
"timestamp": "2023-11-12T20:31:23.751+00:00",
"subErrors": []
},
{
"status": 400,
"message": "No vacant dates available from 2023-11-16 to 2023-11-17",
"timestamp": "2023-11-12T20:31:23.756+00:00",
"subErrors": []
}
]
Execute the concurrent-update-booking.sh script to simulate concurrent updates for the same booking:
$ ./script/test/concurrent-update-booking.sh 2023-11-15 2023-11-16 http:/localhost:80
The response should be as follows after formatting, i.e., only one booking was updated:
[
{
"uuid": "ea10008b-c60e-41f9-97bb-313e9502e7f4",
"version": 1,
"campsiteId": 3,
"email": "[email protected]",
"fullName": "John Smith 1",
"startDate": "2023-11-15",
"endDate": "2023-11-16",
"active": true
},
{
"status": 409,
"message": "Optimistic locking error: com.kiroule.campsitebooking.repository.entity.BookingEntity with id 1 was updated by another transaction",
"timestamp": "2023-11-12T20:29:55.008+00:00",
"subErrors": []
}
]
Basic load testing for retrieving vacant dates can be performed with the ApacheBench by executing the following command:
$ docker-compose.yml up -d
$ ab -n 10000 -c 100 -k http://localhost:80/api/v2/booking/vacant-dates
- -n 10000 is the number of requests to make
- -c 100 is the number of concurrent requests to make at a time
- -k sends the KeepAlive header, which asks the web server to not shut down the connection after each request is done, but to instead keep reusing it
Result:
Benchmarking localhost (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests
Server Software:
Server Hostname: localhost
Server Port: 80
Document Path: /api/v2/booking/vacant-dates
Document Length: 159 bytes
Concurrency Level: 100
Time taken for tests: 2.134 seconds
Complete requests: 10000
Failed requests: 0
Non-2xx responses: 10000
Keep-Alive requests: 0
Total transferred: 2720000 bytes
HTML transferred: 1590000 bytes
Requests per second: 4685.95 [#/sec] (mean)
Time per request: 21.340 [ms] (mean)
Time per request: 0.213 [ms] (mean, across all concurrent requests)
Transfer rate: 1244.70 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 1 0.8 1 7
Processing: 2 20 9.6 19 88
Waiting: 1 19 9.0 18 87
Total: 2 21 9.3 19 88
Percentage of the requests served within a certain time (ms)
50% 19
66% 23
75% 25
80% 27
90% 32
95% 38
98% 47
99% 55
100% 88 (longest request)
Continuous integration is implemented using GitHub Actions, and it includes
the Build Master Branch
, Build on Pull Request
, Generate README TOC
, and Perform Release
workflows:
This workflow is executed automatically on any commit to the master
branch and consists of
the SonarCloud Scan
and Snapshot Publishing
jobs:
This workflow is executed automatically on any pull request and consists of
the Unit & SonarCloud Scan
job:
This workflow is executed automatically on any update of the readme/README.md
file pushed to
the master
branch and consists of the Generate TOC
job:
This workflow is executed manually and consists of the Maven Release
and Docker Image
jobs:
The Release Version
parameter value should be provided before executing this workflow: