Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Fast Prototyping: Building of the Monolith

DZone's Guide to

Fast Prototyping: Building of the Monolith

Learn how to bootstrap your monolith, define interfaces, declare a REST API, and more in Part 2 of this microservices series.

· Microservices Zone ·
Free Resource

Learn why microservices are breaking traditional APM tools that were built for monoliths.

The article is the next in a series describing the proposed approach to system prototyping using microservice architecture.

  1. Fast prototyping: from monolith to microservices

  2. Fast prototyping: building of the monolith (this one)

  3. Fast prototyping: breaking up of the monolith

Before proceeding to further reading, I encourage you to read the assumptions and the general concept described in the first article.

Note: This article is quite long. For this reason, the example source codes are limited to the necessary minimum (without full error handling, comments, tests, etc.), but are fully functional.

Monolith Bootstrapping

To build the system, we will use a ready-to-use Cricket Microsite service, providing some of the functionalities required from our system, implemented as a set of adapters.

Before we get to building our monolith, first a few words about the necessary tools. Installation of JDK 8 or 10 and Apache Ant are required. Ant is my preferred building tool for many reasons. The syntax of the build.xml file is easy to understand, does not force us to adapt to one or another convention, and the whole is well documented.

Start by downloading the Cricket Microsite distribution, unpacking it in the selected folder and then calling the ant program, indicating the tasks to be performed.

$ mkdir example1
$ cd example1
$ wget https://github.com/gskorupa/Cricket/releases/download/1.2.45/cricket-microsite.zip
$ unzip cricket-microsite.zip
$ ant bootstrap
$ ant complie 

The ant bootstrapcommand creates a folder structure with sample files, used in the process of building your own solution. Then ant compilecompiles an example service from the file src/java/my/example/Microsite.java.

Now you can run a local version of the service with the following command

$ sh run-local.sh

Check that there are no error messages in the terminal window and that you have access to the web interfaces http://localhost:8080/and http://localhost:8080/admin

The launched distribution provides out-of-the-box:

  • User and access management

  • Content management

  • Web apps hosting

Those interested in the description of these functionalities I recommend my previous publications - the links are provided at the end of the article.

Extending the Service

The microkernel implemented by the Microsite class will be the basis for creating the planned monolith. We will now add the missing elements by creating:

  1. Task Manager interface

  2. Notifier interface

  3. Dedicated Event class

  4. REST API declaration in the configuration file

  5. Classes implementing defined interfaces

  6. The code to handle REST API by the microkernel

  7. The code to handle the service events by the microkernel

  8. Web application GUI for task management

The source code of the described system is hosted on GitHub.

Defining Interfaces

We start by defining the TaskManagerIface interface

// TaskManagerIface.java
package my.example;
import java.util.List;
import org.cricketmsf.out.db.KeyValueDBIface;

public interface TaskManagerIface {   
    public Task create(Task task);
    public Task read(String id);
    public Task update(Task task);
    public boolean delete(String id);
    public List getAll();
    public void init(KeyValueDBIface database);   
}

Then we define very simple NotifierIface interface.

// NotifierIface.java
package my.example;
import org.cricketmsf.in.http.Result;

public interface NotifierIface {   
    public Result send(String message);   
}

Task-Related Events

A dedicated class named my.example.TaskRelatedEvent will be useful for handling task related events.

// TaskRelatedEvent.java
package my.example;
import org.cricketmsf.Event;

public class TaskRelatedEvent extends Event{
    public static String CATEGORY_TICKETING="TICKETING";

    public TaskRelatedEvent(String payload) {
        setCategory(CATEGORY_TICKETING);
        setPayload(payload);
    }

    public static String category(){
        return CATEGORY_TICKETING;
    }
}

REST API Declaration

Adapters to be used must first be configured. For this purpose, in the src/config.jsonfile we add their definitions in the adapters section.

{
  "Notifier": {
    "name": "Notifier",
    "interfaceName": "NotifierIface",
    "classFullName": "my.example.SlackNotifier",
    "properties": {
        "url": "https://hooks.slack.com/services/YOUR_SLACK_WEEBHOOK/GOES_HERE",
        "ignore-certificate-check":"true"
    }
  },
  "TaskManager": {
    "name": "TaskManager",
    "interfaceName": "TaskManagerIface",
    "classFullName": "my.example.TaskManager",

    "properties": {
    }
  },
  "TaskService": {
    "name": "TaskService",
    "interfaceName": "HttpAdapterIface",
    "classFullName": "org.cricketmsf.in.http.StandardHttpAdapter",
     "properties": {
         "context": "/api/task",
         "silent-mode": "false"
     }
  }
}

As you can see, in each case we set:

  • the full name of the class implementing the given interface

  • list of parameters used by the implementing class

In our example, there is an additional definition of the TaskServiceadapter, implemented by the StandardHttpAdapterclass. This is one of the standard Cricket library adapters used to configure the REST API. The adapter will handle the path /api/taskof the embedded HTTP server. It will be later linked to our Task Manager.

Implementation of the Interfaces

The TaskManagerclass will be implemented using the key-value database available on the platform. This database works in memory and offers saving of data to the file on the service shutdown and their reading at startup.

// TaskManager.java
package my.example;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.cricketmsf.Event;
import org.cricketmsf.Kernel;
import org.cricketmsf.out.OutboundAdapter;
import org.cricketmsf.out.db.KeyValueDBException;
import org.cricketmsf.out.db.KeyValueDBIface;

public class TaskManager extends OutboundAdapter implements TaskManagerIface {

    private KeyValueDBIface database;
    private String tableName = "tasks";

    @Override
    public void init(KeyValueDBIface database) {
        this.database = database;
        try {
            database.addTable("tasks", 0, true);
        } catch (KeyValueDBException ex) {
            Kernel.getInstance()
              .dispatchEvent(Event.logInfo(this, ex.getMessage()));
        }
    }

    @Override
    public Task create(Task task) {
        try {
            if (!database.containsKey(tableName, task.id)) {
                database.put(tableName, task.id, task);
                return (Task) database.get(tableName, task.id);
            }else{
                return null;
            }
        } catch (KeyValueDBException ex) {
            Kernel.getInstance()
              .dispatchEvent(Event.logSevere(this, ex.getMessage()));
            return null;
        }
    }

    @Override
    public Task read(String id) {
        try {
            return (Task) database.get(tableName, id);
        } catch (KeyValueDBException ex) {
            Kernel.getInstance()
              .dispatchEvent(Event.logInfo(this, ex.getMessage()));
        }
        return null;
    }

    @Override
    public Task update(Task task) {
        try {
            if (database.containsKey(tableName, task.id)) {
                database.put(tableName, task.id, task);
                return (Task) database.get(tableName, task.id);
            }else{
                return null;
            }
        } catch (KeyValueDBException ex) {
            Kernel.getInstance()
              .dispatchEvent(Event.logSevere(this, ex.getMessage()));
            return null;
        }

    }

    @Override
    public boolean delete(String id) {
        try {
            return database.remove(tableName, id);
        } catch (KeyValueDBException ex) {
            Kernel.getInstance()
              .dispatchEvent(Event.logSevere(this, ex.getMessage()));
        }
        return false;
    }

    @Override
    public List getAll() {
        ArrayList list = new ArrayList();
        try {
            Map map = database.getAll(tableName);
            map.keySet().forEach(
                    key -> list.add(map.get(key))
            );
        } catch (KeyValueDBException ex) {
            Kernel.getInstance()
              .dispatchEvent(Event.logSevere(this, ex.getMessage()));
        }
        return list;
    }

}

Next, we implement the Notificatorinterface for the selected communication method. We will use the Slack messenger whose WebHooks API integration interface is very convenient and easy to implement.

// SlackNotifier.java
package my.example;

import java.util.HashMap;
import org.cricketmsf.in.http.Result;
import org.cricketmsf.out.http.HttpClient;
import org.cricketmsf.out.http.Request;

public class SlackNotifier extends HttpClient implements NotifierIface{

    private String token;

    @Override
    public void loadProperties(HashMap<String, String> properties, String adapterName){
        super.loadProperties(properties, adapterName);
    }

    @Override
    public Result send(String message) {
        String data="{\"text\":\""+message+"\"}";
        Request request = new Request()
                .setMethod("POST")
                .setUrl(endpointURL)
                .setProperty("Content-type", "application/json")
                .setData(data);
        return send(request);
    }

}

Handling REST API Requests

Having all the required components ready, we can connect them and implement the REST API to manage tasks. To do this, we modify the microkernel code in the Microsite.javafile.

We declare and initialize adapters:

// Microsite.java
public class Microsite extends Kernel {

    // ...
    NotifierIface notifier = null;
    TaskManagerIface taskManager = null;

    @Override
    public void getAdapters() {
        // standard Cricket adapters
        // ...
        // Notifier and Task Manager
        notifier = (NotifierIface) getRegistered("Notifier");
        taskManager = (TaskManagerIface) getRegistered("TaskManager");
        // The microkernel does not call the TaskService adapter 
        // so there is no need to declare a variable for it
    }

    @Override
    public void runInitTasks() {
        // ...
        taskManager.init(database);
        // ...
    }

    // ...

Then we should handle the incoming HTTP requests appropriately.

// Microsite.java

// ...
    @HttpAdapterHook(adapterName = "TaskService", requestMethod = "OPTIONS")
    public Object taskCors(Event requestEvent) {
        StandardResult result = new StandardResult();
        result.setCode(HttpAdapter.SC_OK);
        return result;
    }

    @HttpAdapterHook(adapterName = "TaskService", requestMethod = "GET")
    public Object taskGet(Event event) {
        StandardResult result= new StandardResult(taskManager.getAll());
        return result;
    }

    @HttpAdapterHook(adapterName = "TaskService", requestMethod = "POST")
    public Object taskAdd(Event event) {
        Task task=new Task((String)event.getRequest().parameters.get("task"));
        StandardResult result= new StandardResult(taskManager.create(task));
        dispatchEvent(new TaskRelatedEvent(task.name));
        return result;
    }

    @HttpAdapterHook(adapterName = "TaskService", requestMethod = "PUT")
    public Object taskUpdate(Event event) {
        String taskID = event.getRequest().pathExt;
        String taskName = event.getRequestParameter("task");
        Task newTask = new Task(taskName);
        newTask.id=taskID;
        return new StandardResult(taskManager.update(newTask));
    }

    @HttpAdapterHook(adapterName = "TaskService", requestMethod = "DELETE")
    public Object taskDelete(Event event) {
        String taskID = event.getRequest().pathExt;
        StandardResult result = new StandardResult();
        if(!taskManager.delete(taskID)){
            result.setCode(400);
        }
        return result;
    }
// ...

As we can see, the link between the TaskServiceadapter and the microkernel method is created by the use of @HttpAdapterHookannotation.

Firing and Handling Events

In the above taskAddmethod, we have an example of firing an event after creating a new task. Depending on the configuration, this event can be sent to an external queue or handled by a dedicated microkernel method. Because we are building a monolith, we will prepare the method in a microkernel.

// Microsite.java
    // ...
    @EventHook(eventCategory = "TICKETING")
    public void handleTicketingEvent(Event event) {
        logAdapter.log(event);
        Result result = notifier.send(""+event.getPayload());
    }
    // ...

Web Application

To build the webapp we will use the RIOT library and the data-api.js library included in the Cricket Microsite distribution. The following code is not a fully functional application. It lacks many necessary functionalities, such as modifying and deleting tasks. This is intentional so that the application code is as small as possible and thus easier to analyze.

// index.html
<html>
    <head>
        <title>Tasks</title>
    </head>
    <body>
        <!-- place the custom tag anywhere inside the body -->
    <tasks title='My tasks'></tasks>
    <!-- include the tag -->
    <script type="riot/tag" src="tasks.tag"></script>
    <!-- include riot.js -->
    <script src="https://cdn.jsdelivr.net/npm/riot@3.1/riot+compiler.min.js"></script>
    <!-- Cricket MSF data script -->
    <script src="/js/data-api.js"></script>
    <!-- mount the tag -->
    <script>riot.mount('*')</script>
</body>
</html>

// tasks.tag
<tasks>
  <h3>{ opts.title }</h3>
  <ul>
    <li each={ item, i in items }>{ item.name }</li>
  </ul>
  <form onsubmit={ submitForm }>
        <input type="text" name="task">
      <button type="submit">Create new task </button>
  </form>
  <style>
    h3 {
      font-size: 14px;
    }
  </style>
  <script>
    var self=this
    self.items = []
    self.on('mount', function(){
      self.getTasks()
    })
    self.getTasks = function () {
      getData('http://localhost:8080/api/task', null, null, self.fillTaskList, self)
    }
    self.fillTaskList = function (text) {
      self.items = JSON.parse(text)
      self.update()
    }
    self.submitForm = function (e) {
        e.preventDefault()
        var fd = new FormData(e.target)
        e.target.reset()
        sendFormData(fd, 'POST', 'http://localhost:8080/api/task', null, self.getTasks, self)
    }
  </script>
</tasks>

The Result

Our monolithic application is ready and provides the expected functionality through the REST API:

  • /api/auth

  • /api/user

  • /api/cm

  • /api/cs

  • /api/task

Web apps:

  • http://localhost:8080/ - reading published content

  • http://localhost:8080/admin - managing users and creating/publishing web content

  • http://localhost:8080/local/index.html - managing tasks

The entire system runs as one monolithic service:

Image title

In the next article, I will describe how this can be broken down into a set of microservices performing separate system functions.

Record growth in microservices is disrupting the operational landscape. Read the Global Microservices Trends report to learn more.

Topics:
microservices ,monolith ,tutorial ,rest api ,software architecture

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}