This is the concluding part of a 2 part article on this subject. Part 1 started with the basic introduction and covered the high level reasons of creating microservices using Xsemble. With the example of the Demo8.Microservices application included with Xsemble, it showed how the end-to-end flow can be shown when all the microservices are implemented using Xsemble. In this part, we shall get into more details with the same example to focus on the interactions between microservices — in particular we shall see two variants, as explained by Robert Witkowski:
- Monolith Frontend: The UI is implemented with one microservice and it makes API calls to other microservices
- Micro Frontend: Each Microservice hosts its own UI
We shall also see how Xsemble’s component decoupling mechanisms intended for making components reusable could be of use to maintain portability of microservices.
The Example Application
The Demo8.Microservices application that is supplied with Xsemble is a brutally simplified e-commerce application. The simplifications are done for ease of running and ease of understanding.
Running the Application
Running the application is easy. Below is the short procedure using a Tomcat server installed on your machine.
- Download Xsemble and open Assembler’s Workbench. Open the project “Demo8.Microservices” in that.
- In File > Project parameters > Layer parameters, configure the path of Tomcat’s webapps folder in the Deployment Directory for both layers (shown in the figure below).
- Burn the application with “Generate war and deploy” option turned on.
- Ensure that the generated war files are copied to the Tomcat’s webapps folder, and start Tomcat server.
- Run the application by visiting the url http://localhost:8080/order in your browser.
In a real life e-commerce application one would need more setup such as at least a database; but such requirements were removed in favor of simplification.
Functionality
When you run the Demo8 example, you see the familiar view of an e-commerce site that lets you buy a mobile.
You could add some mobiles to your shopping cart and then click the “View Cart” link to view your shopping cart.
You could remove an item by using “Remove” button.
Xsemble Flow Diagram
Reproduced below is the Xsemble flow diagram of Demo8.Microservices project (with layerwise coloring).
Because it contains two services interacting with one another, namely Ordering (yellow) and ShoppingCart (purple), it gives a good opportunity to examine the caller and callee side implementations.
Service Request
Understanding the Leading Flow
Let’s start tracing the flow. Keep in mind that the control flow is shown with gray lines and the data flow is shown in blue dashed lines. It always begins at the START node (top left in this diagram). The “Load Items Method” then loads the items (products) and passes them to the “List Items” page node. This shows the “Buy Mobile” webpage in the browser with the items loaded.
When the “Add to Cart” button is clicked, the entry point node “Add Item to Cart using API” gets invoked on the server, and it takes the information about the cart item to the server. The “Call Add Item to Cart” method instance is then invoked, which makes an AJAX call to the ShoppingCart microservice.
Making a Service Request
In the component implementation corresponding to “Call Add Item to Cart” method instance (namely CallToApi.java), you will find the usual code that Java programmers use for this purpose:
HttpURLConnection con;
try {
// Creating a Request
URL url = new URL("http://localhost:8080/cart/x/additems");
con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
// Adding Request Parameters
Map<String, String> parameters = new HashMap<>();
parameters.put("price", String.valueOf(cart_items_in.getPrice()));
parameters.put("quantity", String.valueOf(cart_items_in.getQuantity()));
parameters.put("product_id", String.valueOf(cart_items_in.getProduct_id()));
parameters.put("name", cart_items_in.getName());
parameters.put("img", cart_items_in.getImg());
parameters.put("session_id", session_id_in);
con.setDoOutput(true);
Receiving a Service Request
This call is received by “Add Items to Cart” entry point instance in the ShoppingCart layer. Note that an entry point stands at the point where control enters a layer, and takes care of any data translations — from HTTP request variables to server side variables. The corresponding component contains the following Java code that is familiar to the java programmers:
cart_item_out = new Demo8.MicroServices.types.CartItem();
cart_item_out.setPrice(Double.parseDouble(request.getParameter("price")));
cart_item_out.setQuantity(Integer.parseInt(request.getParameter("quantity")));
cart_item_out.setProduct_id(Integer.parseInt(request.getParameter("product_id")));
cart_item_out.setImg(request.getParameter("img"));
cart_item_out.setName(request.getParameter("name"));
session_id_out = request.getParameter("session_id");
The point of showing the code is simply to show that it is the same usual Java code that powers the components, and that programmers have control over it. However, this code is contained within logically separated small components, and so a programmer has to deal with a small number of lines of code at any time, and never the complete code base of the application. The ease of handling by orders of magnitude is the main USP of this approach.
One may observe that these two nodes are tightly coupled with one another, because there needs to be an agreement between the URL and the request parameter. This is usually not seen as a problem in the microservices world, because the URL and the parameters constitute the interface between the two layers, and interfaces are expected to be well-defined and agreed upon. However, we can do even better with Xsemble, as we shall see a little later.
The Trailing Flow
Here are the steps how it goes further.
- The entry point instance populates the received session id and the cart item in data nodes that are grouped under the “add item” data group node.
- “Add items to cart” method instance is then invoked. It takes the values from the data group node and populates the “shopping cart” implemented as a data node with application scope. (The application scope data node stays global in scope within the microservice. In case you wanted multiple instances of the microservice to share the data, you may want to use another common storage mechanism such as a shared redis cache.)
- The control then goes to the “Add Item to Cart Ajax Page” page instance node. The response gets returned to the node that instantiated the HTTP request, namely the method instance “Call Add Item to Cart” method in the “Ordering” layer. (The response is not HTML code but a lightweight response typical of an AJAX call.)
- The node “Call Add Item to Cart” then passes the control to the “API Response Ajax Page” node, and its output in turn becomes the response to the requestor of the HTTP request, namely the “List Items” page instance. (Even this is a lightweight AJAX response.)
- Based on the response, the “List Items” page instance shows an alert indicating success: “Product added to cart”.
This completes our detailing of the functionality to add a cart item to the shopping cart.
UI Servicing Options
Monolith Frontend
We saw how the “Ordering” microservice made an API call to the “ShoppingCart” microservice. The UI is served by the “Ordering” microservice. One could imagine how this thought can be extended further by making the “Ordering” service exclusively own the part of serving the UI, making API calls to other microservices as required. In other words, it becomes a facade to front-end the delivery of all functionality. This is the “Monolith Frontend” pattern implementation.
Micro Frontend
Note that the purple background of the “View Cart Items Page” node in the Demo8.Microservices flow diagram indicates that the node is served directly from the “ShoppingCart” layer. This page node is responsible for showing the “Shopping Cart” webpage. Thus, this is an example where the “ShoppingCart” microservice is serving its own UI. This is the “Micro Frontend” pattern implementation with Xsemble.
Context Tracking
In a scaled up deployment of a real life application, there could be several instances of each of these microservices. Because microservices are stateless and independent, one needs to make sure that the right context is passed to them.
In the Demo8.Microservices example, a session id is passed from the initiating service (the “Ordering” service) to track the user session. The session id is passed to the “ShoppingCart” service while adding an item to the cart, so that the item is tracked against that session id. Then the same session id is passed to the “View Cart” entry point instance which leads to the Shopping Cart webpage. This is done via the “Retrieve Cart Items” method which retrieves the cart items corresponding to the session id. This ensures that, in the scenario multiple users are using the application, their shopping carts are maintained separately as expected.
Eliminating Coupling
Understanding The Coupling
Let’s get back to the discussion on coupling between the microservices. Refer to the code snippet of the caller, and see that it contains the hardcoded URL of the callee, namely
http://localhost:8080/cart/x/additems
What would happen if the callee microservice were deployed at some other base URL (instead of “http://localhost:8080/cart”)? What if the API syntax changes the relative part to something other than “/x/additems”? In both these cases, we would need to go back and change the code, isn’t it? This is the effect of coupling.
While making the change sounds like a simple task, it can become daunting if the complexity of the mesh of microservices grows. (Check this article for some extreme examples of the complex meshes.) The microservice base URL would usually change from developer environment to staging and then to production. If one did not remember to change it after testing it in developer environment, you could get into a situation that the code that works perfectly in the developer environment breaks on staging / production. In other words, your services will not be portable.
What is said above for the URL holds equally true for the names of the parameters, as one needs agreement between the caller and callee on them as well.
Removing the Coupling
A way to handle this situation could be to use a service discovery tool. In absence of using such a tool, one could think of using a configuration file and read the information about end-points from that. However, the mechanism inbuilt into Xsemble to eliminate component coupling can be of help here, especially in situations such as Demo8.Microservices application where the microservices are represented in a single flow diagram.
The Reference Guide that comes with Xsemble has a chapter titled “Making Components Reusable” which explains these mechanisms in detail. Below we just show the application to decoupling the URL.
Note that the signature of the run() method of the component underlying the method instance “Call Add Item to Cart” (in CallToApi.java) is:
public Map<String, Object> run(Map<String, Object> inargs, String currentSession, String currentNode,
Map<String, String> params, Map<String, String> externalExitpath2urlMap,
Map<String, String> externalOutarg2varnameMap)
The argument externalExitpath2urlMap that is passed to the method has this URL “http://localhost:8080/cart/x/additems” against key “next”, which means that one could replace the line:
URL url = new URL("http://localhost:8080/cart/x/additems");
with
URL url = new URL(externalExitpath2UrlMap.get("next"));
Doing so gives it the advantages of decoupling.
Understanding the Mechanism
Let’s see what is cooking under the hood to understand and appreciate this better.
- The base URL where the microservice is deployed is a layer parameter. The “Project Parameters” dialog box in the Assembler’s Workbench (reproduced below) can let you examine and configure the value of the parameter. What is more, if you configure the parameter’s “Setting At” attribute to “Runtime” or “Any”, then this value setting is deferred to runtime, and where the value will be picked up from a configuration file xsemble-layers.properties that gets automatically generated.
- The relative part of the URL is set at the entry point instance level as “URI”. In the Assembler’s Workbench, double-click the node “Add Items to Cart Entry” to examine that the URI is indeed set to “/x/additems” (as shown below). In case the API needs to change, then this is the place where one would change it.
- While burning the application, Xsemble realizes that the “next” exit path of the method instance “Call Add Item to Cart” connects to “Add Items to Cart Entry” entry point. It generates code to construct the complete URL by combining the base URL of the layer and the URI of the entry point instance, and passes the constructed URL in the map named “externalExitPath2UrlMap” against the exit path.
Thus, getting the URL from the map will make your code robust against any changes to the URL.
A similar mechanism exists for the names. Instead of using hardcoded names of the parameters, the programmer should retrieve them from another map passed, namely “externalOutarg2varnameMap”, and then you are guarded against the parameter name changes.
Other Advantages
Component Reuse
Many Xsemble applications feature component reuse. As the implementation code stays at the component level, using the same component in multiple places basically means code reuse. Code reuse results in saving the development and testing effort.
Demo8.Microservices application shows component reuse in two places:
- The entry point instances “Add Item to Cart using API” and “Add Items to Cart” are based on the same entry point component “Add Items To Cart Entry”. (The base component can be seen by double-clicking the node.) What is further interesting is that these entry point instances belong to different layers.
- The node “Add Item to Cart Ajax Page” is reused as the end point of two different requests, one for adding items to cart and the other for removing items from a cart.
Ease of Changing the Design
The most important benefit of Xsemble is the ease of changing the design at will visually, which increases the ease and accuracy.
The layers are defined within a project, and can be viewed and edited in the “Edit Project” dialog (File > Edit project in Assembler’s Workbench) as covered in part 1 of this article. While they are distinct in the nodes of flow diagram, their underlying base components (as seen in the Developer’s Workbench) have no such distinction. Indeed, a component ( “Add Items To Cart Entry”) is a base component of two different nodes belonging to different layers as we saw above.
This means that you may just change the layers of the nodes, and re-engineer an existing monolithic application into one with microservices, or the other way round (if needed), or refactor functionality among microservices to a good extent — all without having to touch the underlying components. Aren’t these possibilities wonderful? Imagine how much trouble they would save!
Summary
With the help of a supplied demo application, we first saw the anatomy of service calls. We saw that the underlying code is pretty much the same as that with a handcoded application; but it is encapsulated in smaller Xsemble components for separation of concerns.
We then saw how Xsemble can be used with both approaches of handling the UI, namely a monolith front end or micro front end, as required. Then we went ahead to examine the context tracking.
We then saw how the component decoupling mechanism inbuilt into Xsemble can help in eliminating the coupling among microservices, making the microservices portable, and making it easier to manage a mesh of them.
Finally, we understood the advantages of component reuse and ease of changing the design by relating them to the demo application.