The idea is to develop a backend-only microservice aggregating data fetched from an already available, third-party API providing information on "users", "products" and "purchases". A single endpoint is exposed combining information so that a list of "recent purchases" a user made is returned including information on the products the user purchased and who else did purchase them, sorted by the number of product purchases.
Information is cached so that further calls take the shortest time and also requirements on not found users are implemented with a custom HTTP response (status code and error message).
- Operating System agnostic.
- JDK 8.
- Maven 3.3.9.
- A decent terminal application and the tools above available in the user PATH.
The requirements above can also be satisfied (vs. plain download + install) using any package management tool for the OS running the demo. Development was made on a Mac and maven was installed via HomeBrew.
I have used the NetBeans IDE for development which ships great Maven integration. But any tool can be used to navigate and study the source code since it is a Maven standard (multi-project) build.
I initially wrote a Java EE based solution including a separate client for the existing API. However I ended up feeling the solution was maybe a bit over engineered for the sole purpose of a single API endpoint. I would take an approach like this one for larger projects but in case it would be considered a "one hammer for all problems", over-engineered solution I wrote a separate implementation on a more straightforward approach based on Vert.x.
The source code includes -and this document explains- both solutions.
Both solutions cache succesful request responses to the consumed API. However these are cached forever because there is no way the consumed service notifies relevant changes or accesses the used cache so that invalidations can take place.
A real-world production solution should take this into account and include some means to allow for invalidating cache entries. Neither of the solutions here have this functionality (though implementing it could be seamless).
In order to build everything, and provided Maven and the JDK tools are in the users path:
cd
to the toplevel directory.- From there, call
$ mvn clean install -DskipTests=true
- Wait until Maven downloads the whole Internet for the first time (subsequent builds won't take this long).
In order not to require a pre-existing deployment of a Java EE (spec.
version 7) application server, I have used the recent
WildFly-Swarm toolset. The idea is to
build an standalone bundle that when run via $ java -jar ...
starts
the required application server components together with the developed
solution.
In order to run it:
$ cd <project-root-level>
$ mvn clean install -DskipTests=true # If not built already
$ cd javaee/server
$ mvn wildfly-swarm:run # Please see (*)
This will run the solution, listening on all interfaces (0.0.0.0
)
and on port 8081
. You can now call the endpoint, for example:
$ curl http://localhost:8081/api/recent_purchases/Kade6
(*) As explained above the solution should also be runnable via a command like:
$ java -jar target/popular-purchases-server-1.0.0.Final-swarm.jar
But this fails due to an ambiguous resolution for injected CDI beans problem. This is most likely a problem with the WildFly Swarm toolset used, which is at the moment a candidate release. I however settled to use it for this demo because running via the Maven plugin works.
I make a rather extensive use of Project Lombok so that lots of the required boilerplate code (constructors, getters and setters mostly) in Java classes is autogenerated at build time. I am a huge fan of this tool and have been using it in production projects for years.
I decided to model the data types and HTTP client for the consumed API
into a separate project within the javaee/daw-purchases-client
directory. The intention is to abstract this API via a dependency the
server
project includes, separating concerns.
This client uses Jackson annotated model clases (package
com.mgl.demo.popularpurchases.dawps.client.model
) and uses the RestEasy Proxy Client
feature to build a REST client for the DAW API from the JAX-RS spec.
in com.mgl.demo.popularpurchases.dawps.client.ApiSpec
.
The core class in this project is
com.mgl.demo.popularpurchases.dawps.client.DawPurchasesClient
. This also configures
the underlying HTTP client with sensible defaults that could be
however made configurable as desired.
The project includes some basic Integration Tests:
$ cd <project-root-level>
$ cd javaee/daw-purchases-client
$ mvn clean install -DskipTests=true # If not built already
$ mvn test # Requires a network connection and the DAW API reachable
Model classes have all final attributes and matching constructors because after all these are just designed to be DTOs and can perfectly be inmutable for our purposes.
This is the main project implementing our endpoint. It depends on the client just explained.
The most relevant classes are:
-
com.mgl.demo.popularpurchases.server.rest.PopularPurchasesResource
: This is the endpoint JAX-RS implementation. It is basically designed to deal with the REST exposure and calls the service below for the business logic. It also deals with HTTP cache including anETag
response header whose value can be used for further client calls via aIf-None-Match
request header. -
com.mgl.demo.popularpurchases.server.service.PopularPurchasesService
: This class implements the core functionality that ends up being exposed in the REST resource above. I am making an extense use of Java 8 features such as lambdas and streams. Also, in order to perform paralell operations, I am using Java 8'sCompletableFuture
(a promises- like API) together with Java EE 7'sManagedExecutorService
. -
com.mgl.demo.popularpurchases.server.service.CacheAwareDawPurchasesClient
: This is a CDI bean that decorates the API client explained before so that it has caching features. Calls to the delegated client are intercepted using JCache API features (@CacheResult
and@Interceptors({CacheResultInterceptor.class})
combined). The underlying cache implementation is Infinispan, for which WildFly Swarm ships support out-of-the-box. -
Selectively mapping method call exceptions into HTTP response codes is made via the
com.mgl.demo.popularpurchases.server.rest.*ExceptionMapper
classes again using JAX-RS features.
Just in case approach #1 seemed a bit too much for a single endpoint (I would however go that route for larger projects) I decided to write a much more straightforward solution based on Vert.x. I have used this project for several production highly-networked systems with fun and success.
A self-runnable jarfile is built:
$ cd <project-root-level>
$ cd vertx
$ mvn clean install -DskipTests=true # If not build already
$ java -jar target/vertx-popular-purchases-server-1.0.0.Final-fat.jar
This will run the solution, listening on all interfaces (0.0.0.0
)
and on port 8002
. You can now call the endpoint, for example:
$ curl http://localhost:8002/api/recent_purchases/Kade6
This is a much more direct solution for which I even did not write any model POJOs, but settled to use Jackson's (shipped with Vert.x for JSON support) classes directly. For a larger solution or handling a richer data model I would certainly use dedicated model classes just like for the Java EE solution.
The most relevant (and almost single) piece of code here is
com.mgl.demo.popularpurchases.vertx.RecentPurchasesHandler
. It all starts at its
only public method void handle(RoutingContext routingContext)
. The
code leverages Vert.x's async nature to accomplish paralell requests.
Also, I decided to use the alternative RxJava oriented platform API
instead of the classical callback. So every async operation (API
calls) is built on top of Observable
s and related operations and
classes.
For caching support I settled for Vert.x's LocalMap
shared data
feature which is capable of persisting immutable values visible only
to the JVM the instance is running at. If a distributed cache was
wanted the cluster-enabled shared data structures could be used.
There are other implementation details I have not got into in this document. I would suggest navigating the source code (a stock NetBeans 8.1 installation would serve perfectly as it understands Maven projects natively). The "most relevant classes" explained in this document can be a good starting point for examination.
There are not many code comments around but for the most important operations implementing the core functionality. I expect the code to turn out communicative enough.
Also looking into the several pom.xml
files can provide more
insights on dependencies and the build structure in general. I tried
to write them with as much care as the code itself as I believe build
processes and automation are of the same importance and deserve the
same attention and maintainability.