Archived 🗂: This was an old code challenge for a job interview |
---|
Shop Service
A basic RESTful(ish) API for a SHOP resource.
Prerequisites
The project expects Java 8 and gradle to be installed.
Usage
Running the Service
./gradlew bootRun # use 'gradlew.bat bootRun' on Windows
The project uses Lombok and works fine from command line but IDEs need some assistance to know about it. Check how to setup your favourite IDE to support Lombok.
Distributing the Service
./gradlew build # use 'gradlew.bat build' on Windows
generates the binary ./build/libs/shop-service-0.0.1-SNAPSHOT.jar
A Dockerfile is also available to build an image shop-service:latest
.
The image build process can be launched using
./gradlew buildDocker # use 'gradlew.bat buildDocker' on windows`
Testing the Service
Java
Some Java unit-tests and integration tests are included. To execute them, run:
./gradlew test # use 'gradlew.bat test' on windows
Postman
A shop-service.postman_collection
file is also available, with a basic test suite.
To run it, use the Postman GUI or if you have npm
installed, install the CLI package with
npm install -g newman
and then execute the test suite
newman run shop-service.postman_collection --delay-request 500
Example of an execution:
❯❯❯ newman run shop-service.postman_collection --delay-request 500
newman
shop-service
→ Get all shops (Empty)
GET http://localhost:8080/shops [200 OK, 228B, 168ms]
✓ Status code is 200
✓ Content-Type is present
→ Add shop without address
POST http://localhost:8080/shops [400 Bad Request, 227B, 38ms]
✓ Status code is 400
✓ Error message is descriptive
→ Add London shop
POST http://localhost:8080/shops [201 Created, 169B, 122ms]
✓ Status code is 201
✓ Resource location is present
→ Get London shop
GET http://localhost:8080/shops/London%20shop [200 OK, 359B, 18ms]
✓ Status code is 200
✓ Version 0 received
→ Get all shops (shops available)
GET http://localhost:8080/shops [200 OK, 463B, 13ms]
✓ Status code is 200
✓ Content-Type is present
✓ Returns 1 shop
✓ HAL self link to list
✓ HAL self link to each resource
→ Update London shop
POST http://localhost:8080/shops [200 OK, 424B, 8ms]
✓ Status code is 200
✓ Resource location is present
✓ Version 0 received
→ Add Edinburgh shop
POST http://localhost:8080/shops [201 Created, 172B, 7ms]
✓ Status code is 201
✓ Resource location is present
→ Add Liverpool shop
POST http://localhost:8080/shops [201 Created, 172B, 7ms]
✓ Status code is 201
✓ Resource location is present
→ Get London shop (should have coordinates)
GET http://localhost:8080/shops/London%20shop [200 OK, 379B, 7ms]
✓ Status code is 200
✓ Version 1 received
✓ Has coordinates
→ Get closest shop to Edinburgh
GET http://localhost:8080/shops?lat=55.959736&lng=-2.789885 [200 OK, 376B, 8ms]
✓ Status code is 200
✓ Edinburgh shop received
→ Get closest shop to Brighton
GET http://localhost:8080/shops?lat=50.8375077&lng=-0.1764013 [200 OK, 379B, 6ms]
✓ Status code is 200
✓ London shop received
┌─────────────────────────┬──────────┬──────────┐
│ │ executed │ failed │
├─────────────────────────┼──────────┼──────────┤
│ iterations │ 1 │ 0 │
├─────────────────────────┼──────────┼──────────┤
│ requests │ 11 │ 0 │
├─────────────────────────┼──────────┼──────────┤
│ test-scripts │ 11 │ 0 │
├─────────────────────────┼──────────┼──────────┤
│ prerequest-scripts │ 0 │ 0 │
├─────────────────────────┼──────────┼──────────┤
│ assertions │ 27 │ 0 │
├─────────────────────────┴──────────┴──────────┤
│ total run duration: 6.2s │
├───────────────────────────────────────────────┤
│ total data received: 1.37KB (approx) │
├───────────────────────────────────────────────┤
│ average response time: 36ms │
└───────────────────────────────────────────────┘
A delay of 500ms is defined in the CLI and should be defined as well when running the test suite from the GUI as The geolocation of the shops is asynchronous, and the location related validations could fail.
Decisions made
Persistence
I used a ConcurrentHashMap
for a quick and dirty in-memory "persistence" solution instead of an H2 DB for example.
If the requirement should keep something similar to this solution, clustering the service would require a switch
to a distributed data structure such as a Hazelcast Map that seems to implement the standard ConcurrentMap
(I haven't actually used Hazelcast
yet)
Geolocation
I considered the geolocation of the shop as an enrichment step, not critical during creation, and best handled asynchronously
to avoid blocking the user request while waiting for the GMaps API to reply.
I also decided to implement a basic versioning system of the entities to avoid possible race conditions during the geolocation.
The com.example.shopService.domain.ShopService.geolocateShop()
method only sets the coordinates of the shop if the version matches.
The distance calculation is using a standard formula called Haversine and the closest shop is calculated with that formula and a List Comparator (The formula offers an approximation in Km between two points. If greater accuracy is needed, other solutions should be envisaged).
Rest
The service is based on Spring HATEOAS and implements some common best practices such as returning the location of a resource on POST or adding links to resources. The service validates the request's body and tries to return meaningful errors.
Comments
It has been a fun exercise. As an MVP and with the time restrictions, I had to find the right balance between quality and features. The service is far from perfect, and unit tests could be largely expanded to cover quite a few edge cases. I'm still doubting if I understood and implemented as expected the response with the previous shop in case of an update.
I'm looking forward to embark in this new and exciting opportunity :)